@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.
- package/AGENTS.md +27 -0
- package/README.md +60 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/notion-as-code/database.d.ts +88 -48
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +132 -45
- package/dist/notion-as-code/database.test.d.ts +2 -0
- package/dist/notion-as-code/database.test.d.ts.map +1 -0
- package/dist/notion-as-code/index.d.ts +2 -2
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +34 -2
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/notion-as-code/schema.d.ts +8 -4
- package/dist/notion-as-code/schema.d.ts.map +1 -1
- package/dist/notion-as-code/views.d.ts +29 -10
- package/dist/notion-as-code/views.d.ts.map +1 -1
- package/dist/sync.d.ts +33 -15
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +12 -0
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.d.ts.map +1 -0
- package/dist/workflow-state.js +107 -0
- package/dist/workflow.d.ts +48 -3
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +111 -4
- package/docs/BUILD.md +96 -0
- package/package.json +1 -1
- package/skills/notion-as-code/SKILL.md +81 -49
- package/skills/sync/SKILL.md +21 -18
- package/skills/workflow/SKILL.md +52 -3
- package/src/cli/build.test.ts +124 -64
- package/src/index.ts +1 -0
- package/src/notion-as-code/database.test.ts +661 -0
- package/src/notion-as-code/database.ts +344 -127
- package/src/notion-as-code/index.ts +16 -1
- package/src/notion-as-code/intents.ts +39 -2
- package/src/notion-as-code/schema.ts +11 -3
- package/src/notion-as-code/views.ts +29 -10
- package/src/sync.test.ts +295 -0
- package/src/sync.ts +85 -21
- package/src/workflow-state.ts +152 -0
- package/src/workflow.test.ts +329 -1
- package/src/workflow.ts +180 -6
|
@@ -1,197 +1,414 @@
|
|
|
1
1
|
import type { Database } from "../database.js";
|
|
2
2
|
import {
|
|
3
3
|
APPS_WORKSPACE_RESOURCE_ID,
|
|
4
|
-
type DataSourceDefinition,
|
|
5
4
|
type DatabaseIntent,
|
|
5
|
+
type DataSourceDefinition,
|
|
6
6
|
type NotionAsCodeIcon,
|
|
7
7
|
type NotionAsCodeProperty,
|
|
8
|
+
type NotionAsCodeSchema,
|
|
8
9
|
type Parent,
|
|
9
10
|
type ResourceId,
|
|
10
11
|
} from "./intents.js";
|
|
11
12
|
import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
|
|
12
13
|
import { recordIntent } from "./recorder.js";
|
|
13
14
|
import { assertParent, assertUserResourceId } from "./resource.js";
|
|
14
|
-
import { createAppsDatabase, type
|
|
15
|
+
import { createAppsDatabase, type AppsSchemaForSchema } from "./schema.js";
|
|
15
16
|
import type { ViewSchema } from "./views.js";
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/** Reuse serialized database intent metadata, excluding identity, parent, and sources. */
|
|
19
|
+
type DatabaseMetadata = Omit<DatabaseIntent, "resourceId" | "parent" | "dataSources">;
|
|
20
|
+
|
|
21
|
+
type NonEmptyViewSchemas = readonly [ViewSchema, ...ViewSchema[]];
|
|
22
|
+
|
|
23
|
+
/** A keyed data-source declaration; the containing map key supplies the serialized source name. */
|
|
24
|
+
export type DataSourceArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
|
|
21
25
|
resourceId: ResourceId;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dataSources?: DataSources;
|
|
25
|
-
views?: readonly ViewSchema[];
|
|
26
|
+
name?: never;
|
|
27
|
+
schema: Schema;
|
|
26
28
|
icon?: NotionAsCodeIcon;
|
|
27
|
-
// TODO: Port cover types.
|
|
28
|
-
cover?: unknown;
|
|
29
|
-
hideDataSourceTitle?: boolean;
|
|
30
|
-
hideDatabaseTitleIfEmpty?: boolean;
|
|
31
|
-
name?: string;
|
|
32
|
-
description?: string;
|
|
33
29
|
};
|
|
34
30
|
|
|
35
|
-
/**
|
|
36
|
-
export type
|
|
37
|
-
DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
|
|
38
|
-
> = Omit<DatabaseArgs<DataSources>, "parent">;
|
|
31
|
+
/** Data sources keyed by the names used to access their handles. */
|
|
32
|
+
export type DataSourceMap = Record<string, DataSourceArgs>;
|
|
39
33
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Single-source inputs require a name for both database and source. `datasources?: never`
|
|
36
|
+
* keeps this form exclusive even when callers pass a predeclared object.
|
|
37
|
+
*/
|
|
38
|
+
type SingleSourceDatabaseOptions<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = Omit<
|
|
39
|
+
DatabaseMetadata,
|
|
40
|
+
"name"
|
|
41
|
+
> & {
|
|
42
|
+
dataSourceResourceId: ResourceId;
|
|
44
43
|
name: string;
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
schema: Schema;
|
|
45
|
+
datasources?: never;
|
|
47
46
|
};
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
>
|
|
48
|
+
/** Top-level single-source inputs may choose a parent; page.addDatabase/teamspace.addDatabase bind it instead. */
|
|
49
|
+
export type SingleSourceDatabaseArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
|
|
50
|
+
SingleSourceDatabaseOptions<Schema> & {
|
|
51
|
+
parent?: Parent;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Multi-source inputs preserve source-map keys. The NoInfer guard makes the
|
|
56
|
+
* emptiness check inspect the inferred map instead of widening it to the broad
|
|
57
|
+
* DataSourceMap constraint.
|
|
58
|
+
*/
|
|
59
|
+
type MultiSourceDatabaseOptions<DataSources extends DataSourceMap = DataSourceMap> =
|
|
60
|
+
DatabaseMetadata & {
|
|
61
|
+
datasources: DataSources;
|
|
62
|
+
dataSourceResourceId?: never;
|
|
63
|
+
schema?: never;
|
|
64
|
+
} & (keyof NoInfer<DataSources> extends never
|
|
65
|
+
? { views: NonEmptyViewSchemas }
|
|
66
|
+
: { views?: readonly ViewSchema[] });
|
|
67
|
+
|
|
68
|
+
/** Top-level multi-source inputs may choose a parent; page.addDatabase/teamspace.addDatabase bind it instead. */
|
|
69
|
+
export type MultiSourceDatabaseArgs<DataSources extends DataSourceMap = DataSourceMap> =
|
|
70
|
+
MultiSourceDatabaseOptions<DataSources> & {
|
|
71
|
+
parent?: Parent;
|
|
72
|
+
};
|
|
52
73
|
|
|
53
|
-
/**
|
|
54
|
-
type
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
/** Linked-only databases use a nonempty typed view tuple and no authored source map. */
|
|
75
|
+
type ViewOnlyDatabaseOptions = DatabaseMetadata & {
|
|
76
|
+
datasources?: never;
|
|
77
|
+
dataSourceResourceId?: never;
|
|
78
|
+
schema?: never;
|
|
79
|
+
views: NonEmptyViewSchemas;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type ViewOnlyDatabaseArgs = ViewOnlyDatabaseOptions & {
|
|
83
|
+
parent?: Parent;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Remove parent because page.addDatabase/teamspace.addDatabase supplies it. */
|
|
87
|
+
export type ChildSingleSourceDatabaseArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
|
|
88
|
+
Omit<SingleSourceDatabaseArgs<Schema>, "parent">;
|
|
89
|
+
|
|
90
|
+
/** Remove parent because page.addDatabase/teamspace.addDatabase supplies it. */
|
|
91
|
+
export type ChildMultiSourceDatabaseArgs<DataSources extends DataSourceMap = DataSourceMap> = Omit<
|
|
92
|
+
MultiSourceDatabaseArgs<DataSources>,
|
|
93
|
+
"parent"
|
|
94
|
+
>;
|
|
95
|
+
|
|
96
|
+
/** Input for declaring a database and its data sources. */
|
|
97
|
+
export type DatabaseArgs<
|
|
98
|
+
Schema extends NotionAsCodeSchema = NotionAsCodeSchema,
|
|
99
|
+
DataSources extends DataSourceMap = DataSourceMap,
|
|
100
|
+
> = SingleSourceDatabaseArgs<Schema> | MultiSourceDatabaseArgs<DataSources> | ViewOnlyDatabaseArgs;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Child inputs omit parent from each union member separately. `page.addDatabase`
|
|
104
|
+
* and `teamspace.addDatabase` use this shape for their shared child factory.
|
|
105
|
+
*/
|
|
106
|
+
export type ChildDatabaseArgs<
|
|
107
|
+
Schema extends NotionAsCodeSchema = NotionAsCodeSchema,
|
|
108
|
+
DataSources extends DataSourceMap = DataSourceMap,
|
|
57
109
|
> =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/** Handle for a data source and the Apps database it backs. */
|
|
65
|
-
export type DataSourceHandle<
|
|
66
|
-
Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
|
|
67
|
-
> = {
|
|
110
|
+
| ChildSingleSourceDatabaseArgs<Schema>
|
|
111
|
+
| ChildMultiSourceDatabaseArgs<DataSources>
|
|
112
|
+
| Omit<ViewOnlyDatabaseArgs, "parent">;
|
|
113
|
+
|
|
114
|
+
/** A source handle retains its keyed authoring schema for sync inference. */
|
|
115
|
+
export type DataSourceHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
|
|
68
116
|
readonly resourceId: string;
|
|
69
|
-
readonly schema:
|
|
70
|
-
readonly database: Database<
|
|
117
|
+
readonly schema: Schema;
|
|
118
|
+
readonly database: Database<AppsSchemaForSchema<Schema>>;
|
|
71
119
|
addPage: (args: ChildPageArgs) => PageHandle;
|
|
72
120
|
};
|
|
73
121
|
|
|
74
|
-
|
|
75
|
-
export type DatabaseHandle<
|
|
76
|
-
DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
|
|
77
|
-
> = {
|
|
122
|
+
type DatabaseHandleBase = {
|
|
78
123
|
readonly resourceId: string;
|
|
79
|
-
readonly dataSources: {
|
|
80
|
-
readonly [ResourceId in DataSources[number]["resourceId"]]: DataSourceHandle<
|
|
81
|
-
DataSourcePropertiesForResourceId<DataSources, ResourceId>
|
|
82
|
-
>;
|
|
83
|
-
};
|
|
84
124
|
addView: (view: ViewSchema) => void;
|
|
85
125
|
};
|
|
86
126
|
|
|
87
|
-
/**
|
|
88
|
-
export type
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
/** Map each authoring source key to a handle backed by that source's own schema. */
|
|
128
|
+
export type DatabaseHandle<DataSources extends DataSourceMap = DataSourceMap> =
|
|
129
|
+
DatabaseHandleBase & {
|
|
130
|
+
readonly datasources: {
|
|
131
|
+
readonly [DataSourceName in keyof DataSources]: DataSourceHandle<
|
|
132
|
+
DataSources[DataSourceName]["schema"]
|
|
133
|
+
>;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
93
136
|
|
|
137
|
+
/** A database whose sole data source is available without a key lookup. */
|
|
138
|
+
export type SingleSourceDatabaseHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
|
|
139
|
+
DatabaseHandleBase & {
|
|
140
|
+
readonly dataSource: DataSourceHandle<Schema>;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Overloaded callable used by page.addDatabase/teamspace.addDatabase. Each form
|
|
145
|
+
* returns its matching handle while preserving literal source and schema inference.
|
|
146
|
+
*/
|
|
94
147
|
export type ChildDatabaseFactory = {
|
|
95
|
-
<const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
148
|
+
<const Schema extends NotionAsCodeSchema>(
|
|
149
|
+
databaseResourceId: ResourceId,
|
|
150
|
+
args: ChildSingleSourceDatabaseArgs<Schema>,
|
|
151
|
+
): SingleSourceDatabaseHandle<Schema>;
|
|
152
|
+
<const DataSources extends DataSourceMap>(
|
|
153
|
+
databaseResourceId: ResourceId,
|
|
154
|
+
args: ChildMultiSourceDatabaseArgs<DataSources>,
|
|
100
155
|
): DatabaseHandle<DataSources>;
|
|
156
|
+
(
|
|
157
|
+
databaseResourceId: ResourceId,
|
|
158
|
+
args: Omit<ViewOnlyDatabaseArgs, "parent">,
|
|
159
|
+
): DatabaseHandle<{}>;
|
|
101
160
|
};
|
|
102
161
|
|
|
103
|
-
/** Declare a database, defaulting to
|
|
104
|
-
export function database<const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
/** Declare a database, defaulting to the Apps workspace. */
|
|
163
|
+
export function database<const Schema extends NotionAsCodeSchema>(
|
|
164
|
+
databaseResourceId: ResourceId,
|
|
165
|
+
args: SingleSourceDatabaseArgs<Schema>,
|
|
166
|
+
): SingleSourceDatabaseHandle<Schema>;
|
|
167
|
+
export function database<const DataSources extends DataSourceMap>(
|
|
168
|
+
databaseResourceId: ResourceId,
|
|
169
|
+
args: MultiSourceDatabaseArgs<DataSources>,
|
|
109
170
|
): DatabaseHandle<DataSources>;
|
|
110
171
|
export function database(
|
|
111
|
-
|
|
172
|
+
databaseResourceId: ResourceId,
|
|
173
|
+
args: ViewOnlyDatabaseArgs,
|
|
174
|
+
): DatabaseHandle<{}>;
|
|
175
|
+
export function database(
|
|
176
|
+
databaseResourceId: ResourceId,
|
|
177
|
+
args: DatabaseArgs,
|
|
112
178
|
): DatabaseHandle | SingleSourceDatabaseHandle {
|
|
113
|
-
if (args.
|
|
114
|
-
|
|
179
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
180
|
+
throw new Error("Notion-as-Code database arguments must be an object");
|
|
115
181
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
182
|
+
const parentValue = args.parent;
|
|
183
|
+
if (parentValue !== undefined) {
|
|
184
|
+
assertParent(parentValue);
|
|
185
|
+
}
|
|
186
|
+
const parent = parentValue ?? {
|
|
187
|
+
type: "resourceId" as const,
|
|
118
188
|
resourceId: APPS_WORKSPACE_RESOURCE_ID,
|
|
119
189
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
return buildDatabaseHandle(databaseResourceId, args, parent);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Bind a parent for page.addDatabase/teamspace.addDatabase. */
|
|
194
|
+
export function createChildDatabase(parent: Parent): ChildDatabaseFactory {
|
|
195
|
+
assertParent(parent);
|
|
196
|
+
|
|
197
|
+
function addDatabase<const Schema extends NotionAsCodeSchema>(
|
|
198
|
+
databaseResourceId: ResourceId,
|
|
199
|
+
args: ChildSingleSourceDatabaseArgs<Schema>,
|
|
200
|
+
): SingleSourceDatabaseHandle<Schema>;
|
|
201
|
+
function addDatabase<const DataSources extends DataSourceMap>(
|
|
202
|
+
databaseResourceId: ResourceId,
|
|
203
|
+
args: ChildMultiSourceDatabaseArgs<DataSources>,
|
|
204
|
+
): DatabaseHandle<DataSources>;
|
|
205
|
+
function addDatabase(
|
|
206
|
+
databaseResourceId: ResourceId,
|
|
207
|
+
args: Omit<ViewOnlyDatabaseArgs, "parent">,
|
|
208
|
+
): DatabaseHandle<{}>;
|
|
209
|
+
function addDatabase(
|
|
210
|
+
databaseResourceId: ResourceId,
|
|
211
|
+
args: ChildDatabaseArgs,
|
|
212
|
+
): DatabaseHandle | SingleSourceDatabaseHandle {
|
|
213
|
+
return buildDatabaseHandle(databaseResourceId, args, parent);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return addDatabase;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Internal bridge pairing an authoring key/schema with its serialized definition
|
|
220
|
+
* during database recording and handle creation; never serialized itself.
|
|
221
|
+
*/
|
|
222
|
+
type SourceEntry = {
|
|
223
|
+
key: string;
|
|
224
|
+
schema: NotionAsCodeSchema;
|
|
225
|
+
definition: DataSourceDefinition;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build a database handle from one of the authoring forms. Form checks happen
|
|
230
|
+
* before normalization or recording so mixed and malformed declarations are atomic.
|
|
231
|
+
*/
|
|
232
|
+
function buildDatabaseHandle(
|
|
233
|
+
databaseResourceId: ResourceId,
|
|
234
|
+
args: DatabaseArgs,
|
|
235
|
+
parent: Parent,
|
|
236
|
+
): DatabaseHandle | SingleSourceDatabaseHandle {
|
|
237
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
238
|
+
throw new Error("Notion-as-Code database arguments must be an object");
|
|
239
|
+
}
|
|
240
|
+
if (typeof databaseResourceId !== "string") {
|
|
241
|
+
throw new Error("Notion-as-Code database resourceId must be a string");
|
|
242
|
+
}
|
|
243
|
+
assertUserResourceId(databaseResourceId);
|
|
244
|
+
|
|
245
|
+
const hasSchema = Object.prototype.hasOwnProperty.call(args, "schema");
|
|
246
|
+
const hasDataSources = Object.prototype.hasOwnProperty.call(args, "datasources");
|
|
247
|
+
if (hasSchema && hasDataSources) {
|
|
248
|
+
throw new Error("Specify either schema or datasources, not both");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (args.schema !== undefined) {
|
|
125
252
|
const source: DataSourceDefinition = {
|
|
126
|
-
resourceId:
|
|
253
|
+
resourceId: args.dataSourceResourceId,
|
|
127
254
|
name: args.name,
|
|
128
|
-
properties,
|
|
255
|
+
properties: normalizeProperties(args.schema),
|
|
129
256
|
};
|
|
130
|
-
const handle =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
257
|
+
const handle = recordDatabase(databaseResourceId, args, parent, [
|
|
258
|
+
{ key: "dataSource", schema: args.schema, definition: source },
|
|
259
|
+
]);
|
|
260
|
+
const dataSource = handle.datasources.dataSource;
|
|
261
|
+
if (dataSource === undefined) {
|
|
262
|
+
throw new Error(`Database "${databaseResourceId}" is missing its data source`);
|
|
134
263
|
}
|
|
135
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
resourceId: databaseResourceId,
|
|
266
|
+
dataSource,
|
|
267
|
+
addView: handle.addView,
|
|
268
|
+
};
|
|
136
269
|
}
|
|
137
|
-
|
|
270
|
+
|
|
271
|
+
if (args.datasources !== undefined) {
|
|
272
|
+
if (
|
|
273
|
+
typeof args.datasources !== "object" ||
|
|
274
|
+
args.datasources === null ||
|
|
275
|
+
Array.isArray(args.datasources)
|
|
276
|
+
) {
|
|
277
|
+
throw new Error("Notion-as-Code datasources must be an object");
|
|
278
|
+
}
|
|
279
|
+
const sourceEntries = Object.entries(args.datasources).map(([key, sourceArgs]) =>
|
|
280
|
+
normalizeDataSource(key, sourceArgs),
|
|
281
|
+
);
|
|
282
|
+
return recordDatabase(databaseResourceId, args, parent, sourceEntries);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return recordDatabase(databaseResourceId, args, parent, []);
|
|
138
286
|
}
|
|
139
287
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
288
|
+
/** Strip authoring-only fields from database arguments before serializing metadata. */
|
|
289
|
+
function normalizeDatabaseMetadata(args: DatabaseArgs): DatabaseMetadata {
|
|
290
|
+
const {
|
|
291
|
+
parent: _parent,
|
|
292
|
+
schema: _schema,
|
|
293
|
+
datasources: _datasources,
|
|
294
|
+
dataSourceResourceId: _dataSourceResourceId,
|
|
295
|
+
...metadata
|
|
296
|
+
} = args;
|
|
297
|
+
return metadata;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Normalize a keyed declaration into its serialized definition while retaining key/schema for handles. */
|
|
301
|
+
function normalizeDataSource(key: string, sourceArgs: DataSourceArgs): SourceEntry {
|
|
302
|
+
if (typeof sourceArgs !== "object" || sourceArgs === null || Array.isArray(sourceArgs)) {
|
|
303
|
+
throw new Error(`Data source "${key}" must be an object`);
|
|
304
|
+
}
|
|
305
|
+
const { schema, ...sourceMetadata } = sourceArgs;
|
|
306
|
+
return {
|
|
307
|
+
key,
|
|
308
|
+
schema,
|
|
309
|
+
definition: {
|
|
310
|
+
...sourceMetadata,
|
|
311
|
+
name: key,
|
|
312
|
+
properties: normalizeProperties(schema),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Normalize keyed property configurations to the serialized property array.
|
|
319
|
+
* Property names always come from schema keys; authoring name fields are ignored.
|
|
320
|
+
*/
|
|
321
|
+
function normalizeProperties(schema: NotionAsCodeSchema): NotionAsCodeProperty[] {
|
|
322
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
323
|
+
throw new Error("Notion-as-Code schema must be an object");
|
|
324
|
+
}
|
|
325
|
+
const properties: NotionAsCodeProperty[] = [];
|
|
326
|
+
for (const [key, propertyConfig] of Object.entries(schema)) {
|
|
327
|
+
if (
|
|
328
|
+
typeof propertyConfig !== "object" ||
|
|
329
|
+
propertyConfig === null ||
|
|
330
|
+
Array.isArray(propertyConfig)
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(`Property "${key}" must be an object`);
|
|
153
333
|
}
|
|
154
|
-
|
|
334
|
+
if (typeof propertyConfig.resourceId !== "string") {
|
|
335
|
+
throw new Error(`Property "${key}" must specify a resourceId`);
|
|
336
|
+
}
|
|
337
|
+
properties.push({
|
|
338
|
+
...propertyConfig,
|
|
339
|
+
name: key,
|
|
340
|
+
});
|
|
155
341
|
}
|
|
156
|
-
return
|
|
342
|
+
return properties;
|
|
157
343
|
}
|
|
158
344
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
345
|
+
/** Validate the views shape and require at least one data source or view. */
|
|
346
|
+
function assertSourceOrView(sourceEntries: readonly SourceEntry[], views: unknown): void {
|
|
347
|
+
if (views !== undefined && !Array.isArray(views)) {
|
|
348
|
+
throw new Error("Notion-as-Code database views must be an array");
|
|
349
|
+
}
|
|
350
|
+
if (sourceEntries.length === 0 && (!Array.isArray(views) || views.length === 0)) {
|
|
351
|
+
throw new Error("Notion-as-Code database must define a data source or view");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Record a normalized database intent and create handles for its data sources. */
|
|
356
|
+
function recordDatabase(
|
|
357
|
+
databaseResourceId: ResourceId,
|
|
358
|
+
args: DatabaseArgs,
|
|
359
|
+
parent: Parent,
|
|
360
|
+
sourceEntries: readonly SourceEntry[],
|
|
361
|
+
): DatabaseHandle {
|
|
362
|
+
const metadata = normalizeDatabaseMetadata(args);
|
|
363
|
+
assertSourceOrView(sourceEntries, metadata.views);
|
|
364
|
+
const intent: DatabaseIntent = {
|
|
365
|
+
...metadata,
|
|
366
|
+
resourceId: databaseResourceId,
|
|
367
|
+
parent,
|
|
368
|
+
dataSources: sourceEntries.map(({ definition }) => definition),
|
|
369
|
+
};
|
|
370
|
+
return createDatabase(intent, sourceEntries);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Create a normalized database handle while preserving each source's authoring schema. */
|
|
374
|
+
function createDatabase(
|
|
375
|
+
intent: DatabaseIntent,
|
|
376
|
+
sourceEntries: readonly SourceEntry[],
|
|
377
|
+
): DatabaseHandle {
|
|
378
|
+
assertUserResourceId(intent.resourceId);
|
|
163
379
|
const dataSourceIds = new Set<string>();
|
|
164
|
-
const handles: Record<string, DataSourceHandle> =
|
|
380
|
+
const handles: Record<string, DataSourceHandle> = Object.create(null);
|
|
165
381
|
|
|
166
|
-
for (const
|
|
167
|
-
|
|
168
|
-
|
|
382
|
+
for (const { key, schema, definition } of sourceEntries) {
|
|
383
|
+
if (typeof definition.resourceId !== "string") {
|
|
384
|
+
throw new Error(`Data source "${key}" must specify a resourceId`);
|
|
385
|
+
}
|
|
386
|
+
assertUserResourceId(definition.resourceId);
|
|
387
|
+
if (dataSourceIds.has(definition.resourceId)) {
|
|
169
388
|
throw new Error(
|
|
170
|
-
`Database "${
|
|
389
|
+
`Database "${intent.resourceId}" has duplicate data source "${definition.resourceId}"`,
|
|
171
390
|
);
|
|
172
391
|
}
|
|
173
|
-
dataSourceIds.add(
|
|
174
|
-
handles[
|
|
392
|
+
dataSourceIds.add(definition.resourceId);
|
|
393
|
+
handles[key] = createDataSourceHandle(definition, schema);
|
|
175
394
|
}
|
|
176
395
|
|
|
177
|
-
recordIntent({
|
|
178
|
-
type: "database",
|
|
179
|
-
...args,
|
|
180
|
-
parent,
|
|
181
|
-
dataSources: [...dataSources],
|
|
182
|
-
} satisfies { type: "database" } & DatabaseIntent);
|
|
396
|
+
recordIntent({ ...intent, type: "database" });
|
|
183
397
|
|
|
184
398
|
return {
|
|
185
|
-
resourceId:
|
|
186
|
-
|
|
399
|
+
resourceId: intent.resourceId,
|
|
400
|
+
datasources: handles,
|
|
187
401
|
addView(view) {
|
|
188
|
-
recordIntent({ type: "view", databaseResourceId:
|
|
402
|
+
recordIntent({ type: "view", databaseResourceId: intent.resourceId, view });
|
|
189
403
|
},
|
|
190
404
|
};
|
|
191
405
|
}
|
|
192
406
|
|
|
193
|
-
/** Create a data
|
|
194
|
-
function createDataSourceHandle(
|
|
407
|
+
/** Create a data-source handle backed by the Apps database used by syncs. */
|
|
408
|
+
function createDataSourceHandle(
|
|
409
|
+
dataSource: DataSourceDefinition,
|
|
410
|
+
schema: NotionAsCodeSchema,
|
|
411
|
+
): DataSourceHandle {
|
|
195
412
|
const propertyResourceIds = new Set<string>();
|
|
196
413
|
const propertyNames = new Set<string>();
|
|
197
414
|
for (const property of dataSource.properties) {
|
|
@@ -212,7 +429,7 @@ function createDataSourceHandle(dataSource: DataSourceDefinition): DataSourceHan
|
|
|
212
429
|
|
|
213
430
|
return {
|
|
214
431
|
resourceId: dataSource.resourceId,
|
|
215
|
-
schema
|
|
432
|
+
schema,
|
|
216
433
|
database: createAppsDatabase(dataSource.resourceId, dataSource),
|
|
217
434
|
addPage(pageArgs) {
|
|
218
435
|
return createPage(pageArgs, { type: "resourceId", resourceId: dataSource.resourceId });
|
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { createNotion } from "./handles.js";
|
|
2
2
|
|
|
3
3
|
export { APPS_RESERVED_RESOURCE_ID_PREFIX, APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
|
|
4
|
-
export type {
|
|
4
|
+
export type {
|
|
5
|
+
DatabaseIntent,
|
|
6
|
+
DataSourceDefinition,
|
|
7
|
+
NotionAsCodeIcon,
|
|
8
|
+
NotionAsCodeProperty,
|
|
9
|
+
NotionAsCodePropertyConfig,
|
|
10
|
+
NotionAsCodeSchema,
|
|
11
|
+
NotionAsCodeSelectOption,
|
|
12
|
+
NotionAsCodeStatusOption,
|
|
13
|
+
Parent,
|
|
14
|
+
ResourceId,
|
|
15
|
+
} from "./intents.js";
|
|
5
16
|
export type { CustomAgentArgs, CustomAgentHandle } from "./custom-agent.js";
|
|
6
17
|
export type {
|
|
7
18
|
ChildDatabaseArgs,
|
|
8
19
|
ChildDatabaseFactory,
|
|
20
|
+
ChildMultiSourceDatabaseArgs,
|
|
9
21
|
ChildSingleSourceDatabaseArgs,
|
|
22
|
+
DataSourceArgs,
|
|
10
23
|
DataSourceHandle,
|
|
24
|
+
DataSourceMap,
|
|
11
25
|
DatabaseArgs,
|
|
12
26
|
DatabaseHandle,
|
|
27
|
+
MultiSourceDatabaseArgs,
|
|
13
28
|
SingleSourceDatabaseArgs,
|
|
14
29
|
SingleSourceDatabaseHandle,
|
|
15
30
|
} from "./database.js";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CustomAgentArgs } from "./custom-agent.js";
|
|
2
|
-
import type { DatabaseArgs } from "./database.js";
|
|
3
2
|
import type { PageArgs } from "./page.js";
|
|
4
3
|
import type { TeamspaceArgs } from "./teamspace.js";
|
|
5
4
|
import type { ViewSchema } from "./views.js";
|
|
@@ -66,6 +65,7 @@ type UnsupportedProperty = PropertyBase & {
|
|
|
66
65
|
[key: string]: unknown;
|
|
67
66
|
};
|
|
68
67
|
|
|
68
|
+
/** A normalized Notion-as-Code property with its resolved display name. */
|
|
69
69
|
export type NotionAsCodeProperty =
|
|
70
70
|
| (PropertyBase & { type: "title" })
|
|
71
71
|
| (PropertyBase & { type: "text" })
|
|
@@ -92,6 +92,28 @@ export type NotionAsCodeProperty =
|
|
|
92
92
|
| (PropertyBase & { type: "file" })
|
|
93
93
|
| UnsupportedProperty;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* `T extends unknown` applies Omit to each property variant separately, preserving
|
|
97
|
+
* variant-specific fields such as required status options. Plain Omit on the union
|
|
98
|
+
* would retain only shared keys. Pick also restores type/resourceId on the
|
|
99
|
+
* unsupported variant, whose string index signature would otherwise erase them.
|
|
100
|
+
*/
|
|
101
|
+
type DistributivePropertyConfig<T extends NotionAsCodeProperty> = T extends unknown
|
|
102
|
+
? Omit<T, "name"> & Pick<T, "type" | "resourceId">
|
|
103
|
+
: never;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Authoring properties are keyed by their schema object name. A property-level
|
|
107
|
+
* `name` override would make the key and serialized property diverge, so it is
|
|
108
|
+
* explicitly rejected even for the string-indexed unsupported-property variant.
|
|
109
|
+
*/
|
|
110
|
+
export type NotionAsCodePropertyConfig = DistributivePropertyConfig<NotionAsCodeProperty> & {
|
|
111
|
+
name?: never;
|
|
112
|
+
};
|
|
113
|
+
/** A keyed property schema, with object keys supplying serialized property names. */
|
|
114
|
+
export type NotionAsCodeSchema = Record<string, NotionAsCodePropertyConfig>;
|
|
115
|
+
|
|
116
|
+
/** The normalized data-source shape emitted in provisioning artifacts. */
|
|
95
117
|
export type DataSourceDefinition<
|
|
96
118
|
Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
|
|
97
119
|
> = {
|
|
@@ -107,9 +129,24 @@ export type TeamspaceIntent = TeamspaceArgs & {
|
|
|
107
129
|
parent: Parent;
|
|
108
130
|
};
|
|
109
131
|
|
|
110
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The normalized database shape emitted in provisioning artifacts.
|
|
134
|
+
*
|
|
135
|
+
* This intentionally does not depend on the authoring argument shape. Authoring
|
|
136
|
+
* accepts keyed schemas and data-source maps; the recorder always stores arrays.
|
|
137
|
+
*/
|
|
138
|
+
export type DatabaseIntent = {
|
|
139
|
+
resourceId: ResourceId;
|
|
111
140
|
parent: Parent;
|
|
112
141
|
dataSources: readonly DataSourceDefinition[];
|
|
142
|
+
views?: readonly ViewSchema[];
|
|
143
|
+
icon?: NotionAsCodeIcon;
|
|
144
|
+
// TODO: Port cover types.
|
|
145
|
+
cover?: unknown;
|
|
146
|
+
hideDataSourceTitle?: boolean;
|
|
147
|
+
hideDatabaseTitleIfEmpty?: boolean;
|
|
148
|
+
name?: string;
|
|
149
|
+
description?: string;
|
|
113
150
|
};
|
|
114
151
|
|
|
115
152
|
export type PageIntent = PageArgs & { parent: Parent };
|