@notionhq/apps 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +60 -35
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/notion-as-code/database.d.ts +88 -48
  6. package/dist/notion-as-code/database.d.ts.map +1 -1
  7. package/dist/notion-as-code/database.js +132 -45
  8. package/dist/notion-as-code/database.test.d.ts +2 -0
  9. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  10. package/dist/notion-as-code/index.d.ts +2 -2
  11. package/dist/notion-as-code/index.d.ts.map +1 -1
  12. package/dist/notion-as-code/intents.d.ts +34 -2
  13. package/dist/notion-as-code/intents.d.ts.map +1 -1
  14. package/dist/notion-as-code/schema.d.ts +8 -4
  15. package/dist/notion-as-code/schema.d.ts.map +1 -1
  16. package/dist/notion-as-code/views.d.ts +29 -10
  17. package/dist/notion-as-code/views.d.ts.map +1 -1
  18. package/dist/sync.d.ts +33 -15
  19. package/dist/sync.d.ts.map +1 -1
  20. package/dist/sync.js +12 -0
  21. package/dist/workflow-state.d.ts +13 -0
  22. package/dist/workflow-state.d.ts.map +1 -0
  23. package/dist/workflow-state.js +107 -0
  24. package/dist/workflow.d.ts +48 -3
  25. package/dist/workflow.d.ts.map +1 -1
  26. package/dist/workflow.js +111 -4
  27. package/docs/BUILD.md +96 -0
  28. package/package.json +1 -1
  29. package/skills/notion-as-code/SKILL.md +81 -49
  30. package/skills/sync/SKILL.md +21 -18
  31. package/skills/workflow/SKILL.md +52 -3
  32. package/src/cli/build.test.ts +124 -64
  33. package/src/index.ts +1 -0
  34. package/src/notion-as-code/database.test.ts +661 -0
  35. package/src/notion-as-code/database.ts +344 -127
  36. package/src/notion-as-code/index.ts +16 -1
  37. package/src/notion-as-code/intents.ts +39 -2
  38. package/src/notion-as-code/schema.ts +11 -3
  39. package/src/notion-as-code/views.ts +29 -10
  40. package/src/sync.test.ts +295 -0
  41. package/src/sync.ts +85 -21
  42. package/src/workflow-state.ts +152 -0
  43. package/src/workflow.test.ts +329 -1
  44. package/src/workflow.ts +180 -6
@@ -15,17 +15,18 @@ import { finishMetadataRecording, startMetadataRecording } from "../notion-as-co
15
15
  describe("Notion-as-Code database shorthand", () => {
16
16
  it("keeps provisioning and sync identity equivalent to an explicit source", () => {
17
17
  const recorder = startMetadataRecording();
18
- const database = notion.database({
19
- resourceId: "issues",
18
+ const schema = {
19
+ Name: { resourceId: "title", type: "title" },
20
+ Count: { resourceId: "count", type: "number" },
21
+ } as const;
22
+ const database = notion.database("issues", {
23
+ dataSourceResourceId: "issues-source",
20
24
  name: "Issues",
21
- properties: [
22
- { resourceId: "title", name: "Name", type: "title" },
23
- { resourceId: "count", name: "Count", type: "number" },
24
- ],
25
+ schema,
25
26
  });
26
- expectTypeOf(database).toExtend<
27
- SingleSourceDatabaseHandle<typeof database.dataSource.schema>
28
- >();
27
+ expectTypeOf(database).toExtend<SingleSourceDatabaseHandle<typeof schema>>();
28
+ expectTypeOf(database.dataSource.schema.Name.type).toEqualTypeOf<"title">();
29
+ expectTypeOf(database.dataSource.schema.Name.resourceId).toEqualTypeOf<"title">();
29
30
  expectTypeOf(
30
31
  database.dataSource.database.config.schema.Count.type,
31
32
  ).toEqualTypeOf<"number">();
@@ -33,18 +34,19 @@ describe("Notion-as-Code database shorthand", () => {
33
34
  const shorthand = finishMetadataRecording(recorder).intents;
34
35
 
35
36
  const explicitRecorder = startMetadataRecording();
36
- const explicit = notion.database({
37
- resourceId: "issues",
37
+ const explicit = notion.database("issues", {
38
38
  name: "Issues",
39
- dataSources: [
40
- {
39
+ datasources: {
40
+ Issues: {
41
41
  resourceId: "issues-source",
42
- name: "Issues",
43
- properties: database.dataSource.schema,
42
+ schema,
44
43
  },
45
- ],
44
+ },
46
45
  });
47
- explicit.dataSources["issues-source"].addPage({ resourceId: "issue" });
46
+ expectTypeOf(explicit.datasources.Issues).toExtend<DataSourceHandle<typeof schema>>();
47
+ expectTypeOf(explicit.datasources.Issues.schema.Name.type).toEqualTypeOf<"title">();
48
+ expectTypeOf(explicit.datasources.Issues.schema.Name.resourceId).toEqualTypeOf<"title">();
49
+ explicit.datasources.Issues.addPage({ resourceId: "issue" });
48
50
  // @ts-expect-error Explicit databases do not guarantee a single source.
49
51
  void explicit.dataSource;
50
52
  expect(shorthand).toEqual(finishMetadataRecording(explicitRecorder).intents);
@@ -56,22 +58,24 @@ describe("Notion-as-Code database shorthand", () => {
56
58
  });
57
59
  });
58
60
 
59
- it("preserves parent binding and inferred source types through child helpers", () => {
61
+ it("preserves parent binding and keyed source types through child helpers", () => {
60
62
  const recorder = startMetadataRecording();
61
63
  const page = notion.page({ resourceId: "page" });
62
64
  const team = notion.teamspace({ resourceId: "team", name: "Team", accessLevel: "private" });
65
+ const schema = {
66
+ Title: { resourceId: "child-title", type: "title" },
67
+ } as const;
63
68
  for (const parent of [page, team]) {
64
- const database = parent.addDatabase({
65
- resourceId: `${parent.resourceId}-db`,
69
+ const database = parent.addDatabase(`${parent.resourceId}-db`, {
70
+ dataSourceResourceId: `${parent.resourceId}-source`,
66
71
  name: "Child",
67
- properties: [
68
- { resourceId: `${parent.resourceId}-title`, name: "Title", type: "title" },
69
- ],
72
+ schema,
70
73
  });
71
- expectTypeOf(database.dataSource).toExtend<
72
- DataSourceHandle<typeof database.dataSource.schema>
73
- >();
74
- expectTypeOf(database.dataSource.schema[0].name).toEqualTypeOf<"Title">();
74
+ expectTypeOf(database.dataSource).toExtend<DataSourceHandle<typeof schema>>();
75
+ expectTypeOf(database.dataSource.schema.Title.type).toEqualTypeOf<"title">();
76
+ expectTypeOf(
77
+ database.dataSource.schema.Title.resourceId,
78
+ ).toEqualTypeOf<"child-title">();
75
79
  }
76
80
  const intents = finishMetadataRecording(recorder).intents;
77
81
  expect(
@@ -82,19 +86,60 @@ describe("Notion-as-Code database shorthand", () => {
82
86
  ]);
83
87
  });
84
88
 
85
- it("rejects mixed forms before recording an intent", () => {
89
+ it("keeps keyed child inference and rejects empty child databases", () => {
86
90
  const recorder = startMetadataRecording();
87
- const declareMixedDatabase = () => {
88
- // @ts-expect-error The shorthand and explicit forms are mutually exclusive.
89
- notion.database({
90
- resourceId: "mixed",
91
- name: "Mixed",
92
- properties: [],
93
- dataSources: [],
94
- });
91
+ const page = notion.page({ resourceId: "child-page" });
92
+ const schema = {
93
+ Name: { type: "title", resourceId: "child-name" },
94
+ "Issue ID": { type: "text", resourceId: "child-issue-id" },
95
+ } as const;
96
+ const database = page.addDatabase("child-multi-database", {
97
+ datasources: {
98
+ Issues: {
99
+ resourceId: "child-issues-source",
100
+ schema,
101
+ },
102
+ },
103
+ });
104
+ expectTypeOf(database.datasources.Issues).toExtend<DataSourceHandle<typeof schema>>();
105
+ expectTypeOf(
106
+ database.datasources.Issues.schema.Name.resourceId,
107
+ ).toEqualTypeOf<"child-name">();
108
+
109
+ const invalidChildDeclarations = () => {
110
+ // @ts-expect-error Child databases need a source or a view.
111
+ page.addDatabase("child-empty-datasources", { datasources: {} });
112
+ // @ts-expect-error Child databases need a source or a view.
113
+ page.addDatabase("child-empty-arguments", {});
114
+ // @ts-expect-error Empty child sources and views are invalid.
115
+ page.addDatabase("child-empty-with-views", { datasources: {}, views: [] });
95
116
  };
96
- expect(declareMixedDatabase).toThrow();
97
- expect(finishMetadataRecording(recorder).intents).toEqual([]);
117
+ void invalidChildDeclarations;
118
+ expect(() =>
119
+ page.addDatabase("child-runtime-empty", { datasources: {}, views: [] } as never),
120
+ ).toThrow();
121
+
122
+ const intents = finishMetadataRecording(recorder).intents;
123
+ expect(intents).toHaveLength(2);
124
+ expect(intents).toEqual(
125
+ expect.arrayContaining([
126
+ expect.objectContaining({
127
+ type: "database",
128
+ resourceId: "child-multi-database",
129
+ parent: { type: "resourceId", resourceId: "child-page" },
130
+ dataSources: [
131
+ {
132
+ resourceId: "child-issues-source",
133
+ name: "Issues",
134
+ properties: [
135
+ { resourceId: "child-name", name: "Name", type: "title" },
136
+ { resourceId: "child-issue-id", name: "Issue ID", type: "text" },
137
+ ],
138
+ },
139
+ ],
140
+ }),
141
+ ]),
142
+ );
98
143
  });
99
144
  });
100
145
 
@@ -163,41 +208,49 @@ describe("buildApp", () => {
163
208
  it("builds a sync manifest with a Notion-as-Code meetings database", async () => {
164
209
  const { manifest } = await buildApp(example("sync"));
165
210
 
166
- expect(manifest).toEqual({
167
- $schema: "notion:apps-manifest:v1",
168
- sdkVersion: SDK_VERSION,
169
- databases: [
170
- {
211
+ expect(manifest).toEqual(
212
+ expect.objectContaining({
213
+ $schema: "notion:apps-manifest:v1",
214
+ sdkVersion: SDK_VERSION,
215
+ xldbs: [],
216
+ pacers: [],
217
+ }),
218
+ );
219
+ expect(manifest.databases).toHaveLength(2);
220
+ expect(manifest.databases).toEqual(
221
+ expect.arrayContaining([
222
+ expect.objectContaining({
171
223
  key: "issues",
172
- config: {
224
+ config: expect.objectContaining({
173
225
  initialTitle: "GitHub Issues",
174
- schema: {
175
- properties: {
226
+ schema: expect.objectContaining({
227
+ properties: expect.objectContaining({
176
228
  Name: { type: "title" },
177
229
  "GitHub ID": { type: "text" },
178
230
  Closed: { type: "checkbox" },
179
- },
180
- },
181
- },
182
- },
183
- {
231
+ }),
232
+ }),
233
+ }),
234
+ }),
235
+ expect.objectContaining({
184
236
  key: "homebase-meetings-db-source",
185
- config: {
237
+ config: expect.objectContaining({
186
238
  initialTitle: "Meetings",
187
- schema: {
188
- properties: {
239
+ schema: expect.objectContaining({
240
+ properties: expect.objectContaining({
189
241
  Name: { type: "title" },
190
242
  "Meeting ID": { type: "text" },
191
243
  "Meeting date": { type: "date" },
192
244
  "Prep complete": { type: "checkbox" },
193
- },
194
- },
195
- },
196
- },
197
- ],
198
- xldbs: [],
199
- pacers: [],
200
- capabilities: [
245
+ }),
246
+ }),
247
+ }),
248
+ }),
249
+ ]),
250
+ );
251
+ expect(manifest.capabilities).toHaveLength(2);
252
+ expect(manifest.capabilities).toEqual(
253
+ expect.arrayContaining([
201
254
  {
202
255
  type: "sync",
203
256
  key: "issues",
@@ -218,8 +271,8 @@ describe("buildApp", () => {
218
271
  schedule: { type: "interval", intervalMs: 3_600_000 },
219
272
  },
220
273
  },
221
- ],
222
- });
274
+ ]),
275
+ );
223
276
  });
224
277
 
225
278
  it("emits a separate Notion-as-Code provisioning artifact", async () => {
@@ -285,6 +338,13 @@ describe("buildApp", () => {
285
338
  resourceId: "__notion_apps_workspace__",
286
339
  },
287
340
  dataSources: [],
341
+ views: [
342
+ {
343
+ resourceId: "github-app-private-view",
344
+ type: "table",
345
+ dataSourceResourceId: "issues-source",
346
+ },
347
+ ],
288
348
  },
289
349
  {
290
350
  type: "database",
package/src/index.ts CHANGED
@@ -6,3 +6,4 @@ 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
8
  export { workflow } from "./workflow.js";
9
+ export type { WorkflowState, WorkflowStateValue } from "./workflow-state.js";