@notionhq/apps 0.0.3 → 0.0.6
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 +47 -2
- package/dist/builder.d.ts +29 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +86 -0
- package/dist/builder.test.d.ts +2 -0
- package/dist/builder.test.d.ts.map +1 -0
- package/dist/cli/build.js +1 -1
- package/dist/cli/discover.d.ts +2 -2
- package/dist/cli/discover.d.ts.map +1 -1
- package/dist/cli/discover.js +2 -1
- package/dist/cli/emit-manifest.d.ts +2 -2
- package/dist/cli/emit-manifest.d.ts.map +1 -1
- package/dist/cli/emit-manifest.js +177 -27
- package/dist/context.js +1 -1
- package/dist/database.d.ts +13 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +9 -0
- package/dist/error.d.ts +7 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +10 -1
- package/dist/manifest.d.ts +30 -5
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -0
- package/dist/pacer.d.ts +15 -0
- package/dist/pacer.d.ts.map +1 -0
- package/dist/pacer.js +63 -0
- package/dist/runtime-input.d.ts +2 -0
- package/dist/runtime-input.d.ts.map +1 -0
- package/dist/runtime-input.js +20 -0
- package/dist/runtime-input.test.d.ts +2 -0
- package/dist/runtime-input.test.d.ts.map +1 -0
- package/dist/schedule.d.ts +3 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +23 -0
- package/dist/schema.d.ts +111 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +39 -0
- package/dist/sync.d.ts +72 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +75 -0
- package/dist/sync.test.d.ts +2 -0
- package/dist/sync.test.d.ts.map +1 -0
- package/dist/triggers.generated.d.ts +1 -1
- package/dist/triggers.generated.d.ts.map +1 -1
- package/dist/triggers.generated.js +1 -1
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +2 -0
- package/docs/BUILD.md +1 -0
- package/package.json +33 -2
- package/src/builder.test.ts +81 -0
- package/src/builder.ts +99 -0
- package/src/cli/build.test.ts +109 -0
- package/src/cli/build.ts +1 -1
- package/src/cli/codegen.test.ts +15 -0
- package/src/cli/discover.test.ts +13 -2
- package/src/cli/discover.ts +3 -2
- package/src/cli/emit-manifest.ts +229 -53
- package/src/context.ts +1 -1
- package/src/database.ts +25 -0
- package/src/error.ts +11 -0
- package/src/manifest.ts +33 -5
- package/src/pacer.ts +82 -0
- package/src/runtime-input.test.ts +65 -0
- package/src/runtime-input.ts +30 -0
- package/src/schedule.ts +25 -0
- package/src/schema.ts +73 -0
- package/src/sync.test.ts +359 -0
- package/src/sync.ts +190 -0
- package/src/triggers.generated.ts +1 -1
- package/src/types.ts +109 -0
- package/src/workflow.test.ts +22 -0
- package/src/workflow.ts +2 -0
package/README.md
CHANGED
|
@@ -7,13 +7,58 @@
|
|
|
7
7
|
3. Run `mise run setup`
|
|
8
8
|
4. Run `mise run check && mise run test`
|
|
9
9
|
|
|
10
|
-
The SDK requires Node 26 or newer
|
|
10
|
+
The SDK requires Node 26 or newer. It supports workflows and database syncs.
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { triggers } from "@notionhq/apps/triggers";
|
|
14
14
|
import { createWorkflow } from "@notionhq/apps/workflow";
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Database syncs
|
|
18
|
+
|
|
19
|
+
Apps declare a database schema, and users attach a real Notion database to it. Apps SDK
|
|
20
|
+
databases are always attached; there is no managed-database option.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Builder } from "@notionhq/apps/builder";
|
|
24
|
+
import { createDatabase } from "@notionhq/apps/database";
|
|
25
|
+
import { Schema } from "@notionhq/apps/schema";
|
|
26
|
+
import { createSync } from "@notionhq/apps/sync";
|
|
27
|
+
|
|
28
|
+
const issues = createDatabase("issues", {
|
|
29
|
+
schema: {
|
|
30
|
+
Name: Schema.title(),
|
|
31
|
+
"GitHub ID": Schema.richText(),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export default createSync({
|
|
36
|
+
database: issues,
|
|
37
|
+
primaryKey: "GitHub ID",
|
|
38
|
+
handler: async () => ({
|
|
39
|
+
changes: [
|
|
40
|
+
{
|
|
41
|
+
type: "upsert",
|
|
42
|
+
key: "123",
|
|
43
|
+
properties: { Name: Builder.title("Fix the bug") },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Put each default-exported sync directly in `src/syncs/`. A primary key must be a title
|
|
51
|
+
or rich-text property. Do not include that property in an upsert: the SDK fills it from
|
|
52
|
+
the change's `key`.
|
|
53
|
+
|
|
54
|
+
### Moving from the Workers alpha SDK
|
|
55
|
+
|
|
56
|
+
- Remove `type` and `primaryKey` from `createDatabase`.
|
|
57
|
+
- Move the property map from `schema.properties` directly into `schema`.
|
|
58
|
+
- Add `primaryKey` to each `createSync`.
|
|
59
|
+
- Remove the primary-key property from each upsert's `properties` object.
|
|
60
|
+
- Change imports from `@notionhq/workers/alpha/*` to `@notionhq/apps/*`.
|
|
61
|
+
|
|
17
62
|
## Publishing New Versions
|
|
18
63
|
|
|
19
64
|
Publishing is initiated only from the GitHub Actions UI:
|
|
@@ -22,5 +67,5 @@ Publishing is initiated only from the GitHub Actions UI:
|
|
|
22
67
|
2. Select **Run workflow** on the `main` branch.
|
|
23
68
|
|
|
24
69
|
The workflow always increments the patch version. It builds, checks, and tests the
|
|
25
|
-
SDK before committing the new version, creating
|
|
70
|
+
SDK before committing the new version, creating a `v*` GitHub release,
|
|
26
71
|
and publishing `@notionhq/apps` to npm.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Cover, Icon, NoticonColor, PeopleValue, PlaceValue, RelationReference, TextValue } from "./types.js";
|
|
2
|
+
/** Build property values returned by a sync handler. */
|
|
3
|
+
export declare const Builder: {
|
|
4
|
+
richText: (content: string) => TextValue;
|
|
5
|
+
text: (content: string) => TextValue;
|
|
6
|
+
title: (content: string) => TextValue;
|
|
7
|
+
url: (url: string) => TextValue;
|
|
8
|
+
email: (email: string) => TextValue;
|
|
9
|
+
phoneNumber: (phone: string) => TextValue;
|
|
10
|
+
checkbox: (checked: boolean) => TextValue;
|
|
11
|
+
file: (url: string, name?: string) => TextValue;
|
|
12
|
+
number: (value: number) => TextValue;
|
|
13
|
+
date(value: string): TextValue;
|
|
14
|
+
dateTime(value: string, timeZone?: string): TextValue;
|
|
15
|
+
dateRange(startDate: string, endDate: string): TextValue;
|
|
16
|
+
dateTimeRange(startValue: string, endValue: string, timeZone?: string): TextValue;
|
|
17
|
+
link: (text: string, url: string) => TextValue;
|
|
18
|
+
select: (value: string) => TextValue;
|
|
19
|
+
multiSelect: (...values: string[]) => TextValue;
|
|
20
|
+
status: (value: string) => TextValue;
|
|
21
|
+
people: (...emails: string[]) => PeopleValue;
|
|
22
|
+
place(value: PlaceValue): PlaceValue;
|
|
23
|
+
relation: (primaryKey: string) => RelationReference;
|
|
24
|
+
emojiIcon: (value: string) => Icon;
|
|
25
|
+
notionIcon: (icon: string, color?: NoticonColor) => Icon;
|
|
26
|
+
imageIcon: (url: string) => Icon;
|
|
27
|
+
imageCover(url: string, position?: number): Cover;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,KAAK,EAEL,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,SAAS,EACT,MAAM,YAAY,CAAC;AAmBpB,wDAAwD;AACxD,eAAO,MAAM,OAAO;wBACC,MAAM,KAAG,SAAS;oBACtB,MAAM,KAAG,SAAS;qBACjB,MAAM,KAAG,SAAS;eACxB,MAAM,KAAG,SAAS;mBACd,MAAM,KAAG,SAAS;yBACZ,MAAM,KAAG,SAAS;wBACnB,OAAO,KAAG,SAAS;gBAC3B,MAAM,SAAS,MAAM,KAAG,SAAS;oBAC7B,MAAM,KAAG,SAAS;gBACtB,MAAM,GAAG,SAAS;oBAId,MAAM,aAAa,MAAM,GAAG,SAAS;yBAShC,MAAM,WAAW,MAAM,GAAG,SAAS;8BAK9B,MAAM,YAAY,MAAM,aAAa,MAAM,GAAG,SAAS;iBAYpE,MAAM,OAAO,MAAM,KAAG,SAAS;oBAC5B,MAAM,KAAG,SAAS;6BACT,MAAM,EAAE,KAAG,SAAS;oBAE7B,MAAM,KAAG,SAAS;wBACd,MAAM,EAAE,KAAG,WAAW;iBAC7B,UAAU,GAAG,UAAU;2BAMb,MAAM,KAAG,iBAAiB;uBAI9B,MAAM,KAAG,IAAI;uBACb,MAAM,UAAS,YAAY,KAAY,IAAI;qBAK7C,MAAM,KAAG,IAAI;oBACd,MAAM,sBAAmB,KAAK;CAM9C,CAAC"}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
function validateDateString(value) {
|
|
2
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(new Date(value).getTime())) {
|
|
3
|
+
throw new Error(`Invalid date: ${value}. Expected YYYY-MM-DD.`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function dateToken(value) {
|
|
7
|
+
return [["\u2023", [["d", value]]]];
|
|
8
|
+
}
|
|
9
|
+
function parseISODateTime(value) {
|
|
10
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) || Number.isNaN(new Date(value).getTime())) {
|
|
11
|
+
throw new Error(`Invalid ISO 8601 datetime: ${value}`);
|
|
12
|
+
}
|
|
13
|
+
return { date: value.slice(0, 10), time: value.slice(11, 16) };
|
|
14
|
+
}
|
|
15
|
+
const Builder = {
|
|
16
|
+
richText: (content) => [[content]],
|
|
17
|
+
text: (content) => [[content]],
|
|
18
|
+
title: (content) => [[content]],
|
|
19
|
+
url: (url) => [[url]],
|
|
20
|
+
email: (email) => [[email]],
|
|
21
|
+
phoneNumber: (phone) => [[phone]],
|
|
22
|
+
checkbox: (checked) => checked ? [["Yes"]] : [["No"]],
|
|
23
|
+
file: (url, name) => [[name ?? url, [["a", url]]]],
|
|
24
|
+
number: (value) => Number.isNaN(value) ? [] : [[String(value)]],
|
|
25
|
+
date(value) {
|
|
26
|
+
validateDateString(value);
|
|
27
|
+
return dateToken({ type: "date", start_date: value });
|
|
28
|
+
},
|
|
29
|
+
dateTime(value, timeZone) {
|
|
30
|
+
const parsed = parseISODateTime(value);
|
|
31
|
+
return dateToken({
|
|
32
|
+
type: "datetime",
|
|
33
|
+
start_date: parsed.date,
|
|
34
|
+
start_time: parsed.time,
|
|
35
|
+
...timeZone === void 0 ? {} : { time_zone: timeZone }
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
dateRange(startDate, endDate) {
|
|
39
|
+
validateDateString(startDate);
|
|
40
|
+
validateDateString(endDate);
|
|
41
|
+
return dateToken({ type: "daterange", start_date: startDate, end_date: endDate });
|
|
42
|
+
},
|
|
43
|
+
dateTimeRange(startValue, endValue, timeZone) {
|
|
44
|
+
const start = parseISODateTime(startValue);
|
|
45
|
+
const end = parseISODateTime(endValue);
|
|
46
|
+
return dateToken({
|
|
47
|
+
type: "datetimerange",
|
|
48
|
+
start_date: start.date,
|
|
49
|
+
start_time: start.time,
|
|
50
|
+
end_date: end.date,
|
|
51
|
+
end_time: end.time,
|
|
52
|
+
...timeZone === void 0 ? {} : { time_zone: timeZone }
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
link: (text, url) => [[text, [["a", url]]]],
|
|
56
|
+
select: (value) => [[value]],
|
|
57
|
+
multiSelect: (...values) => values.length === 0 ? [] : [[values.join(",")]],
|
|
58
|
+
status: (value) => [[value]],
|
|
59
|
+
people: (...emails) => emails.map((email) => ({ email })),
|
|
60
|
+
place(value) {
|
|
61
|
+
if (!Number.isFinite(value.lat) || !Number.isFinite(value.lon)) {
|
|
62
|
+
throw new Error("Place value must have numeric lat and lon coordinates");
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
},
|
|
66
|
+
relation: (primaryKey) => ({
|
|
67
|
+
type: "primaryKey",
|
|
68
|
+
value: primaryKey
|
|
69
|
+
}),
|
|
70
|
+
emojiIcon: (value) => ({ type: "emoji", value }),
|
|
71
|
+
notionIcon: (icon, color = "gray") => ({
|
|
72
|
+
type: "notion",
|
|
73
|
+
icon,
|
|
74
|
+
color
|
|
75
|
+
}),
|
|
76
|
+
imageIcon: (url) => ({ type: "image", url }),
|
|
77
|
+
imageCover(url, position = 0.5) {
|
|
78
|
+
if (!Number.isFinite(position) || position < 0 || position > 1) {
|
|
79
|
+
throw new Error("Cover position must be between 0 and 1");
|
|
80
|
+
}
|
|
81
|
+
return { type: "image", url, position };
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
export {
|
|
85
|
+
Builder
|
|
86
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.test.d.ts","sourceRoot":"","sources":["../src/builder.test.ts"],"names":[],"mappings":""}
|
package/dist/cli/build.js
CHANGED
|
@@ -8,7 +8,7 @@ async function buildApp(projectRoot) {
|
|
|
8
8
|
const capabilities = await discoverCapabilities(projectRoot);
|
|
9
9
|
if (capabilities.length === 0) {
|
|
10
10
|
throw new Error(
|
|
11
|
-
`No capabilities found in ${projectRoot}. Add a workflow
|
|
11
|
+
`No capabilities found in ${projectRoot}. Add a workflow under src/workflows/ or a sync under src/syncs/.`
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
const entryPath = path.join(projectRoot, ".notion", "entry.ts");
|
package/dist/cli/discover.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { CapabilityType } from "../manifest.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* A capability type the build tool knows how to discover: which directory
|
|
4
4
|
* under `src/` its files live in, and the `_tag` its default exports must
|
|
5
|
-
* carry. This SDK
|
|
5
|
+
* carry. This SDK registers workflows and syncs.
|
|
6
6
|
*/
|
|
7
7
|
export type CapabilityTypeDefinition = {
|
|
8
8
|
type: CapabilityType;
|
|
@@ -26,7 +26,7 @@ export type DiscoveredCapability = {
|
|
|
26
26
|
* `src/<directory>/` for each registered capability type. Subdirectories are
|
|
27
27
|
* not searched — they hold plain modules, not capabilities.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* Workflow and sync files are discovered from their matching directories.
|
|
30
30
|
*/
|
|
31
31
|
export declare function discoverCapabilities(projectRoot: string): Promise<DiscoveredCapability[]>;
|
|
32
32
|
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/cli/discover.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACtC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAAS,wBAAwB,
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/cli/discover.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACtC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAAS,wBAAwB,EAG/D,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAiC/F"}
|
package/dist/cli/discover.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
const CAPABILITY_TYPES = [
|
|
4
|
-
{ type: "workflow", directory: "workflows", tag: "workflow" }
|
|
4
|
+
{ type: "workflow", directory: "workflows", tag: "workflow" },
|
|
5
|
+
{ type: "sync", directory: "syncs", tag: "sync" }
|
|
5
6
|
];
|
|
6
7
|
async function discoverCapabilities(projectRoot) {
|
|
7
8
|
const discovered = [];
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { AppManifest } from "../manifest.js";
|
|
2
2
|
import type { DiscoveredCapability } from "./discover.js";
|
|
3
|
-
/** Extract the
|
|
3
|
+
/** Extract the app manifest from a built bundle. */
|
|
4
4
|
export declare function extractManifest(bundlePath: string, capabilities: readonly DiscoveredCapability[]): Promise<AppManifest>;
|
|
5
5
|
/**
|
|
6
6
|
* Throw if `value` contains anything `JSON.stringify` would silently drop or
|
|
7
|
-
* rewrite, so invalid
|
|
7
|
+
* rewrite, so invalid manifest configuration fails during the build.
|
|
8
8
|
*/
|
|
9
9
|
export declare function assertJsonSafe(value: unknown, path: string): void;
|
|
10
10
|
//# sourceMappingURL=emit-manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emit-manifest.d.ts","sourceRoot":"","sources":["../../src/cli/emit-manifest.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"emit-manifest.d.ts","sourceRoot":"","sources":["../../src/cli/emit-manifest.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAA6C,MAAM,gBAAgB,CAAC;AAE7F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AA0M1D,oDAAoD;AACpD,wBAAsB,eAAe,CACpC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,SAAS,oBAAoB,EAAE,GAC3C,OAAO,CAAC,WAAW,CAAC,CA6EtB;AAkCD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAoCjE"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { Ajv } from "ajv";
|
|
4
|
+
import { APPS_MANIFEST_SCHEMA } from "../manifest.js";
|
|
5
|
+
import { clearRegisteredPacers, registeredPacers } from "../pacer.js";
|
|
4
6
|
const SDK_VERSION = (() => {
|
|
5
7
|
const require2 = createRequire(import.meta.url);
|
|
6
8
|
const packageJson = require2("../../package.json");
|
|
@@ -12,16 +14,55 @@ const SDK_VERSION = (() => {
|
|
|
12
14
|
const MANIFEST_SCHEMA = {
|
|
13
15
|
type: "object",
|
|
14
16
|
properties: {
|
|
17
|
+
$schema: { type: "string", enum: [APPS_MANIFEST_SCHEMA] },
|
|
15
18
|
sdkVersion: { type: "string" },
|
|
16
|
-
databases: {
|
|
19
|
+
databases: {
|
|
20
|
+
type: "array",
|
|
21
|
+
items: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
key: { type: "string" },
|
|
25
|
+
config: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
initialTitle: { type: "string" },
|
|
29
|
+
schema: { type: "object" }
|
|
30
|
+
},
|
|
31
|
+
required: ["schema"],
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
required: ["key", "config"],
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
17
39
|
xldbs: { type: "array", maxItems: 0 },
|
|
18
|
-
pacers: {
|
|
40
|
+
pacers: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
key: { type: "string" },
|
|
46
|
+
config: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
allowedRequests: { type: "number", exclusiveMinimum: 0 },
|
|
50
|
+
intervalMs: { type: "number", exclusiveMinimum: 0 }
|
|
51
|
+
},
|
|
52
|
+
required: ["allowedRequests", "intervalMs"],
|
|
53
|
+
additionalProperties: false
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
required: ["key", "config"],
|
|
57
|
+
additionalProperties: false
|
|
58
|
+
}
|
|
59
|
+
},
|
|
19
60
|
capabilities: {
|
|
20
61
|
type: "array",
|
|
21
62
|
items: {
|
|
22
63
|
type: "object",
|
|
23
64
|
properties: {
|
|
24
|
-
type: { type: "string",
|
|
65
|
+
type: { type: "string", enum: ["workflow", "sync"] },
|
|
25
66
|
key: { type: "string" },
|
|
26
67
|
config: {}
|
|
27
68
|
},
|
|
@@ -30,29 +71,147 @@ const MANIFEST_SCHEMA = {
|
|
|
30
71
|
}
|
|
31
72
|
}
|
|
32
73
|
},
|
|
33
|
-
required: ["sdkVersion", "databases", "xldbs", "pacers", "capabilities"],
|
|
74
|
+
required: ["$schema", "sdkVersion", "databases", "xldbs", "pacers", "capabilities"],
|
|
34
75
|
additionalProperties: false
|
|
35
76
|
};
|
|
77
|
+
function asTagged(value) {
|
|
78
|
+
if (typeof value === "object" && value !== null && "_tag" in value && typeof value._tag === "string" && "config" in value && typeof value.config === "object" && value.config !== null) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
function asDatabase(value) {
|
|
84
|
+
const tagged = asTagged(value);
|
|
85
|
+
if (tagged?._tag === "database" && "key" in tagged && typeof tagged.key === "string") {
|
|
86
|
+
return tagged;
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function schemaProperties(database) {
|
|
91
|
+
const schema = database.config.schema;
|
|
92
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
93
|
+
throw new Error(`Database "${database.key}" must have a schema object`);
|
|
94
|
+
}
|
|
95
|
+
return schema;
|
|
96
|
+
}
|
|
97
|
+
function isRelationProperty(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && "type" in value && value.type === "relation" && "database" in value;
|
|
99
|
+
}
|
|
100
|
+
class DatabaseCollector {
|
|
101
|
+
#keys = /* @__PURE__ */ new Map();
|
|
102
|
+
#byKey = /* @__PURE__ */ new Map();
|
|
103
|
+
#order = [];
|
|
104
|
+
collect(value, referencedFrom) {
|
|
105
|
+
const known = this.#keys.get(value);
|
|
106
|
+
if (known !== void 0) return known;
|
|
107
|
+
const database = asDatabase(value);
|
|
108
|
+
if (!database) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`${referencedFrom}: expected a database created with createDatabase(...)`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const duplicate = this.#byKey.get(database.key);
|
|
114
|
+
if (duplicate && duplicate !== database) {
|
|
115
|
+
throw new Error(`Two distinct databases use the key "${database.key}"`);
|
|
116
|
+
}
|
|
117
|
+
this.#keys.set(database, database.key);
|
|
118
|
+
this.#byKey.set(database.key, database);
|
|
119
|
+
this.#order.push(database.key);
|
|
120
|
+
for (const [propertyName, property] of Object.entries(schemaProperties(database))) {
|
|
121
|
+
if (isRelationProperty(property)) {
|
|
122
|
+
this.collect(
|
|
123
|
+
property.database,
|
|
124
|
+
`relation property "${propertyName}" of database "${database.key}"`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return database.key;
|
|
129
|
+
}
|
|
130
|
+
get(key) {
|
|
131
|
+
return this.#byKey.get(key);
|
|
132
|
+
}
|
|
133
|
+
entries() {
|
|
134
|
+
return this.#order.map((key) => {
|
|
135
|
+
const database = this.#byKey.get(key);
|
|
136
|
+
const properties = {};
|
|
137
|
+
for (const [propertyName, property] of Object.entries(schemaProperties(database))) {
|
|
138
|
+
if (isRelationProperty(property)) {
|
|
139
|
+
const { database: target, ...rest } = property;
|
|
140
|
+
properties[propertyName] = {
|
|
141
|
+
...rest,
|
|
142
|
+
relatedDatabaseKey: this.#keys.get(target)
|
|
143
|
+
};
|
|
144
|
+
} else {
|
|
145
|
+
properties[propertyName] = property;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
key,
|
|
150
|
+
config: {
|
|
151
|
+
...typeof database.config.initialTitle === "string" ? { initialTitle: database.config.initialTitle } : {},
|
|
152
|
+
schema: {
|
|
153
|
+
properties,
|
|
154
|
+
...database.config.databaseIcon === void 0 ? {} : { databaseIcon: database.config.databaseIcon },
|
|
155
|
+
...database.config.subItems === void 0 ? {} : { subItems: database.config.subItems }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
36
162
|
async function extractManifest(bundlePath, capabilities) {
|
|
163
|
+
clearRegisteredPacers();
|
|
37
164
|
const bundle = await importBundle(bundlePath);
|
|
38
|
-
const
|
|
165
|
+
const registry = getCapabilityRegistry(bundle);
|
|
166
|
+
const databases = new DatabaseCollector();
|
|
167
|
+
const primaryKeyByDatabase = /* @__PURE__ */ new Map();
|
|
39
168
|
const capabilityEntries = capabilities.map((capability) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (!workflow) {
|
|
169
|
+
const tagged = asTagged(registry[capability.type]?.[capability.key]);
|
|
170
|
+
if (!tagged || tagged._tag !== capability.tag) {
|
|
43
171
|
throw new Error(
|
|
44
|
-
`${capability.sourcePath}: default export is not a
|
|
172
|
+
`${capability.sourcePath}: default export is not a ${capability.type} \u2014 files under this directory must default-export create${capitalize(capability.type)}(...)`
|
|
45
173
|
);
|
|
46
174
|
}
|
|
47
|
-
|
|
48
|
-
|
|
175
|
+
let config = { ...tagged.config };
|
|
176
|
+
if (capability.type === "sync") {
|
|
177
|
+
const databaseKey = databases.collect(config.database, capability.sourcePath);
|
|
178
|
+
const database = databases.get(databaseKey);
|
|
179
|
+
const primaryKey = config.primaryKeyProperty;
|
|
180
|
+
const property = typeof primaryKey === "string" ? schemaProperties(database)[primaryKey] : void 0;
|
|
181
|
+
if (typeof primaryKey !== "string" || typeof property !== "object" || property === null || !("type" in property) || property.type !== "title" && property.type !== "text") {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`${capability.sourcePath}: primaryKey "${String(primaryKey)}" must name a title or rich-text property`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const existingPrimaryKey = primaryKeyByDatabase.get(databaseKey);
|
|
187
|
+
if (existingPrimaryKey !== void 0 && existingPrimaryKey !== primaryKey) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Syncs for database "${databaseKey}" disagree on its primary key: "${existingPrimaryKey}" and "${primaryKey}"`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
primaryKeyByDatabase.set(databaseKey, primaryKey);
|
|
193
|
+
const { database: _database, ...rest } = config;
|
|
194
|
+
config = { ...rest, databaseKey };
|
|
195
|
+
}
|
|
196
|
+
assertJsonSafe(config, `${capability.sourcePath} config`);
|
|
197
|
+
return { type: capability.type, key: capability.key, config };
|
|
49
198
|
});
|
|
199
|
+
const pacerEntries = registeredPacers().map((pacer) => ({
|
|
200
|
+
key: pacer.name,
|
|
201
|
+
config: pacer.config
|
|
202
|
+
}));
|
|
203
|
+
for (const pacer of pacerEntries) assertJsonSafe(pacer.config, `pacer "${pacer.key}" config`);
|
|
204
|
+
const databaseEntries = databases.entries();
|
|
205
|
+
for (const database of databaseEntries) {
|
|
206
|
+
assertJsonSafe(database.config, `database "${database.key}" config`);
|
|
207
|
+
}
|
|
50
208
|
const manifest = JSON.parse(
|
|
51
209
|
JSON.stringify({
|
|
210
|
+
$schema: APPS_MANIFEST_SCHEMA,
|
|
52
211
|
sdkVersion: SDK_VERSION,
|
|
53
|
-
databases:
|
|
212
|
+
databases: databaseEntries,
|
|
54
213
|
xldbs: [],
|
|
55
|
-
pacers:
|
|
214
|
+
pacers: pacerEntries,
|
|
56
215
|
capabilities: capabilityEntries
|
|
57
216
|
})
|
|
58
217
|
);
|
|
@@ -68,8 +227,7 @@ async function importBundle(bundlePath) {
|
|
|
68
227
|
return await import(`${pathToFileURL(bundlePath).href}?import=${importNonce++}`);
|
|
69
228
|
} catch (error) {
|
|
70
229
|
throw new Error(
|
|
71
|
-
`Failed to import built app bundle.
|
|
72
|
-
Caused by: ${error instanceof Error ? error.message : String(error)}`
|
|
230
|
+
`Failed to import built app bundle. Capability modules must be importable without secrets or network access. Caused by: ${error instanceof Error ? error.message : String(error)}`
|
|
73
231
|
);
|
|
74
232
|
}
|
|
75
233
|
}
|
|
@@ -79,11 +237,8 @@ function getCapabilityRegistry(bundle) {
|
|
|
79
237
|
}
|
|
80
238
|
return bundle.capabilities;
|
|
81
239
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
return void 0;
|
|
240
|
+
function capitalize(value) {
|
|
241
|
+
return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
87
242
|
}
|
|
88
243
|
function assertJsonSafe(value, path) {
|
|
89
244
|
const reject = (reason) => {
|
|
@@ -91,13 +246,9 @@ function assertJsonSafe(value, path) {
|
|
|
91
246
|
};
|
|
92
247
|
switch (typeof value) {
|
|
93
248
|
case "function":
|
|
94
|
-
reject("function");
|
|
95
|
-
break;
|
|
96
249
|
case "symbol":
|
|
97
|
-
reject("symbol");
|
|
98
|
-
break;
|
|
99
250
|
case "bigint":
|
|
100
|
-
reject(
|
|
251
|
+
reject(typeof value);
|
|
101
252
|
break;
|
|
102
253
|
case "number":
|
|
103
254
|
if (!Number.isFinite(value)) reject(String(value));
|
|
@@ -121,8 +272,7 @@ function assertJsonSafe(value, path) {
|
|
|
121
272
|
reject(object.constructor?.name ?? "non-plain object");
|
|
122
273
|
}
|
|
123
274
|
for (const [key, property] of Object.entries(object)) {
|
|
124
|
-
if (property
|
|
125
|
-
assertJsonSafe(property, `${path}.${key}`);
|
|
275
|
+
if (property !== void 0) assertJsonSafe(property, `${path}.${key}`);
|
|
126
276
|
}
|
|
127
277
|
}
|
|
128
278
|
export {
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Client } from "@notionhq/client";
|
|
2
2
|
const FIX_STEPS = "\n\nTo fix this:\n1. Create a personal access token at https://app.notion.com/developers/tokens\n2. Add NOTION_API_TOKEN=<your-token> to your .env file\n3. For deployed apps, run: ntn apps env push";
|
|
3
3
|
function missingTokenMessage() {
|
|
4
|
-
return "NOTION_API_TOKEN is not set. context.notion requires an API token to make requests
|
|
4
|
+
return "NOTION_API_TOKEN is not set. context.notion requires an API token to make requests from app capabilities to Notion." + FIX_STEPS;
|
|
5
5
|
}
|
|
6
6
|
function createUnauthenticatedNotionProxy() {
|
|
7
7
|
return new Proxy({}, {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DatabaseSchema, DatabaseSchemaOptions } from "./schema.js";
|
|
2
|
+
export type DatabaseConfiguration<S extends DatabaseSchema = DatabaseSchema> = DatabaseSchemaOptions & {
|
|
3
|
+
initialTitle?: string;
|
|
4
|
+
schema: S;
|
|
5
|
+
};
|
|
6
|
+
export type Database<S extends DatabaseSchema = DatabaseSchema> = {
|
|
7
|
+
_tag: "database";
|
|
8
|
+
key: string;
|
|
9
|
+
config: DatabaseConfiguration<S>;
|
|
10
|
+
};
|
|
11
|
+
/** Declare a Notion database that a user will attach to this app. */
|
|
12
|
+
export declare function createDatabase<const S extends DatabaseSchema>(key: string, config: DatabaseConfiguration<S>): Database<S>;
|
|
13
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAC1E,qBAAqB,GAAG;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,CAAC,CAAC;CACV,CAAC;AAEH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAAI;IACjE,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC;CACjC,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EAC5D,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC9B,QAAQ,CAAC,CAAC,CAAC,CAMb"}
|
package/dist/database.js
ADDED
package/dist/error.d.ts
CHANGED
|
@@ -5,4 +5,11 @@ export declare class ExecutionError extends Error {
|
|
|
5
5
|
readonly cause: unknown;
|
|
6
6
|
constructor(cause: unknown);
|
|
7
7
|
}
|
|
8
|
+
/** Tell the platform that an external service asked the sync to slow down. */
|
|
9
|
+
export declare class RateLimitError extends ExecutionError {
|
|
10
|
+
readonly retryAfter: number | undefined;
|
|
11
|
+
constructor(options?: {
|
|
12
|
+
retryAfter?: number;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
8
15
|
//# sourceMappingURL=error.d.ts.map
|
package/dist/error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,SAAkB,KAAK,EAAE,OAAO,CAAC;gBAErB,KAAK,EAAE,OAAO;CAK1B"}
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,SAAkB,KAAK,EAAE,OAAO,CAAC;gBAErB,KAAK,EAAE,OAAO;CAK1B;AAED,8EAA8E;AAC9E,qBAAa,cAAe,SAAQ,cAAc;IACjD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE5B,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;CAK7C"}
|
package/dist/error.js
CHANGED
|
@@ -6,6 +6,15 @@ class ExecutionError extends Error {
|
|
|
6
6
|
this.cause = cause;
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
class RateLimitError extends ExecutionError {
|
|
10
|
+
retryAfter;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
super("Rate limited by external API");
|
|
13
|
+
this.name = "RateLimitError";
|
|
14
|
+
this.retryAfter = options?.retryAfter;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
export {
|
|
10
|
-
ExecutionError
|
|
18
|
+
ExecutionError,
|
|
19
|
+
RateLimitError
|
|
11
20
|
};
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { NormalizedSchedule } from "./types.js";
|
|
2
|
+
/** Identifies the schema used to validate this manifest independently of the SDK package version. */
|
|
3
|
+
export declare const APPS_MANIFEST_SCHEMA: "notion:apps-manifest:v1";
|
|
4
|
+
/** Capability types supported by this SDK. */
|
|
5
|
+
export type CapabilityType = "workflow" | "sync";
|
|
6
|
+
export type DatabaseManifestConfig = {
|
|
7
|
+
initialTitle?: string;
|
|
8
|
+
schema: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type DatabaseManifestEntry = {
|
|
11
|
+
key: string;
|
|
12
|
+
config: DatabaseManifestConfig;
|
|
13
|
+
};
|
|
14
|
+
export type PacerManifestEntry = {
|
|
15
|
+
key: string;
|
|
16
|
+
config: {
|
|
17
|
+
allowedRequests: number;
|
|
18
|
+
intervalMs: number;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type SyncManifestConfig = {
|
|
22
|
+
databaseKey: string;
|
|
23
|
+
primaryKeyProperty: string;
|
|
24
|
+
mode?: "replace" | "incremental";
|
|
25
|
+
schedule?: NormalizedSchedule;
|
|
26
|
+
};
|
|
3
27
|
/**
|
|
4
28
|
* One capability in the manifest: its type, its key (derived from its
|
|
5
29
|
* filename), and its handler-free configuration.
|
|
@@ -15,11 +39,12 @@ export type CapabilityManifestEntry = {
|
|
|
15
39
|
* complete, static description of the app.
|
|
16
40
|
*/
|
|
17
41
|
export type AppManifest = {
|
|
42
|
+
$schema: typeof APPS_MANIFEST_SCHEMA;
|
|
18
43
|
sdkVersion: string;
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
databases: DatabaseManifestEntry[];
|
|
45
|
+
/** Reserved platform field; the Apps SDK does not support XLDB. */
|
|
21
46
|
xldbs: never[];
|
|
22
|
-
pacers:
|
|
47
|
+
pacers: PacerManifestEntry[];
|
|
23
48
|
capabilities: CapabilityManifestEntry[];
|
|
24
49
|
};
|
|
25
50
|
//# sourceMappingURL=manifest.d.ts.map
|