@eightstate/escli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/notion/block/trash.js +71 -0
- package/dist/commands/notion/comments/add.js +48 -0
- package/dist/commands/notion/comments/get.js +42 -0
- package/dist/commands/notion/comments/list.js +55 -0
- package/dist/commands/notion/comments/reply.js +45 -0
- package/dist/commands/notion/comments/thread.js +87 -0
- package/dist/commands/notion/db/create.js +555 -0
- package/dist/commands/notion/db/query.js +451 -0
- package/dist/commands/notion/db/row/create.js +74 -0
- package/dist/commands/notion/db/row/update.js +165 -0
- package/dist/commands/notion/ds/create.js +73 -0
- package/dist/commands/notion/enroll.js +302 -0
- package/dist/commands/notion/index.js +24 -0
- package/dist/commands/notion/page/edit.js +73 -0
- package/dist/commands/notion/page/move.js +59 -0
- package/dist/commands/notion/page/read.js +60 -0
- package/dist/commands/notion/page/replace-content.js +80 -0
- package/dist/commands/notion/page/replace-text.js +80 -0
- package/dist/commands/notion/page/replace.js +63 -0
- package/dist/commands/notion/page/trash.js +79 -0
- package/dist/commands/notion/search.js +207 -0
- package/dist/commands/notion/upload/attach.js +105 -0
- package/dist/commands/notion/upload/index.js +129 -0
- package/dist/commands/notion/upload/list.js +78 -0
- package/dist/commands/notion/upload/status.js +76 -0
- package/dist/commands/notion/view/create.js +78 -0
- package/dist/commands/notion/whoami.js +96 -0
- package/dist/commands/research.js +11 -0
- package/dist/entry.js +12 -6
- package/dist/io/render-kv.js +12 -0
- package/dist/io/render-labeled.js +16 -0
- package/dist/io/render-table.js +19 -0
- package/dist/io/render-trailer.js +7 -0
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/notion/comments/shared.js +366 -0
- package/dist/lib/notion/db-row/common.js +367 -0
- package/dist/lib/notion/manifest-pass.js +4 -0
- package/dist/lib/notion/page/content-common.js +473 -0
- package/dist/lib/notion/trash-move/support.js +300 -0
- package/dist/lib/notion/upload/shared.js +372 -0
- package/dist/lib/registry.js +118 -25
- package/dist/services/notion.js +274 -0
- package/dist/services/research.js +31 -10
- package/oclif.manifest.json +4084 -1
- package/package.json +22 -17
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
4
|
+
import { ExitCodes } from '@eightstate/contracts/exit-codes';
|
|
5
|
+
import { BaseCommand } from '../../../base-command.js';
|
|
6
|
+
import { EscliError } from '../../../lib/escli-error.js';
|
|
7
|
+
import { writeStdout, writeStderr } from '../../../io/io.js';
|
|
8
|
+
import { renderKv } from '../../../io/render-kv.js';
|
|
9
|
+
import { renderTable } from '../../../io/render-table.js';
|
|
10
|
+
import { NOTION_VERSION, proxy } from '../../../services/notion.js';
|
|
11
|
+
import { hasManifestFlag } from '../../../lib/notion/manifest-pass.js';
|
|
12
|
+
export default class NotionDbCreate extends BaseCommand {
|
|
13
|
+
static errors = [
|
|
14
|
+
ErrorCode.UsageRequired,
|
|
15
|
+
ErrorCode.UsageInvalid,
|
|
16
|
+
ErrorCode.ValidationFailed,
|
|
17
|
+
ErrorCode.JsonInvalid,
|
|
18
|
+
ErrorCode.FileNotFound,
|
|
19
|
+
ErrorCode.FileReadFailed,
|
|
20
|
+
ErrorCode.AuthRequired,
|
|
21
|
+
ErrorCode.NotionUnauthorized,
|
|
22
|
+
ErrorCode.NotionRestricted,
|
|
23
|
+
ErrorCode.NotionNotFound,
|
|
24
|
+
ErrorCode.NotionRateLimited,
|
|
25
|
+
ErrorCode.NotionValidation,
|
|
26
|
+
ErrorCode.NotionConflict,
|
|
27
|
+
ErrorCode.NotionTimeoutUncertain,
|
|
28
|
+
ErrorCode.NotionEnrollmentRequired,
|
|
29
|
+
ErrorCode.NotionBatchPartial,
|
|
30
|
+
ErrorCode.GateInvalidResponse,
|
|
31
|
+
ErrorCode.NetworkError,
|
|
32
|
+
ErrorCode.NetworkTimeout,
|
|
33
|
+
ErrorCode.ServiceUnavailable,
|
|
34
|
+
ErrorCode.ApiError,
|
|
35
|
+
];
|
|
36
|
+
static summary = 'Create a Notion database and initial data source';
|
|
37
|
+
static description = 'Create a database container under a parent, create its initial data source schema, and optionally request initial views.';
|
|
38
|
+
static examples = [
|
|
39
|
+
'<%= config.bin %> notion db create "Launch CRM" --parent 24f104cd477e80afbc30000bd28de8f9 --schema crm.schema.json --data-source-name Leads --view table:"All leads"',
|
|
40
|
+
'<%= config.bin %> notion db create "Launch CRM" --parent 24f104cd477e80afbc30000bd28de8f9 --schema crm.schema.json --dry-run',
|
|
41
|
+
'<%= config.bin %> --json notion db create "Launch CRM" --parent 24f104cd477e80afbc30000bd28de8f9 --schema crm.schema.json',
|
|
42
|
+
];
|
|
43
|
+
static aliases = ['notion database create', 'n db create', 'n database create'];
|
|
44
|
+
static flags = {
|
|
45
|
+
parent: Flags.string({ description: 'Parent page ID, page URL, or wiki database ID.', required: true }),
|
|
46
|
+
'schema-input': Flags.string({ description: 'Schema JSON path, inline JSON, or - for stdin.', required: true, helpValue: 'schema' }),
|
|
47
|
+
'data-source-name': Flags.string({ description: 'Initial data source name. Defaults to the database title.' }),
|
|
48
|
+
view: Flags.string({ description: 'Initial view as <type:name>.', multiple: true }),
|
|
49
|
+
'view-file': Flags.string({ description: 'JSON file containing richer view specs.' }),
|
|
50
|
+
raw: Flags.boolean({ description: 'Emit verbatim Notion response body.', default: false }),
|
|
51
|
+
'dry-run': Flags.boolean({ description: 'Validate inputs and print the create payload without writing to Notion.', default: false }),
|
|
52
|
+
};
|
|
53
|
+
static args = {
|
|
54
|
+
title: Args.string({ description: 'Database title.', required: true }),
|
|
55
|
+
};
|
|
56
|
+
static enableJsonFlag = true;
|
|
57
|
+
static strict = true;
|
|
58
|
+
async run() {
|
|
59
|
+
if (hasManifestFlag(this.argv)) {
|
|
60
|
+
await super.run();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
normalizeSchemaFlagArgv(this.argv);
|
|
64
|
+
if (this.argv.includes('--raw')) {
|
|
65
|
+
await this.execute();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await super.run();
|
|
69
|
+
}
|
|
70
|
+
async execute() {
|
|
71
|
+
const { args, flags } = await this.parse(NotionDbCreate);
|
|
72
|
+
const typedFlags = flags;
|
|
73
|
+
const schema = await parseSchemaInput(typedFlags['schema-input']);
|
|
74
|
+
const dataSourceName = typedFlags['data-source-name'] ?? args.title;
|
|
75
|
+
const views = [...parseViewFlags(typedFlags.view), ...await parseViewFileInput(typedFlags['view-file'])];
|
|
76
|
+
const request = {
|
|
77
|
+
parent: typedFlags.parent,
|
|
78
|
+
title: args.title,
|
|
79
|
+
initial_data_source: { name: dataSourceName, schema },
|
|
80
|
+
views,
|
|
81
|
+
};
|
|
82
|
+
if (flags['dry-run']) {
|
|
83
|
+
await maybeDryRunWarning(flags, 'database create');
|
|
84
|
+
const data = dryRunData('database', request);
|
|
85
|
+
if (!typedFlags.json) {
|
|
86
|
+
await writeStdout(`${renderDbCreate(data)}\n`);
|
|
87
|
+
this.humanOutputHandled = true;
|
|
88
|
+
}
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
const result = await proxy('POST', '/v1/databases', { body: request });
|
|
92
|
+
const payload = await emitRawOrReturn(result, typedFlags.raw);
|
|
93
|
+
const data = normalizeDbCreateResponse(payload, args.title, typedFlags.parent, dataSourceName);
|
|
94
|
+
if (!typedFlags.json && !typedFlags.raw) {
|
|
95
|
+
await writeStdout(`${renderDbCreate(data)}\n`);
|
|
96
|
+
this.humanOutputHandled = true;
|
|
97
|
+
}
|
|
98
|
+
return { ...data, raw: result.raw };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export const notionCreateErrors = [
|
|
102
|
+
ErrorCode.UsageRequired,
|
|
103
|
+
ErrorCode.UsageInvalid,
|
|
104
|
+
ErrorCode.ValidationFailed,
|
|
105
|
+
ErrorCode.JsonInvalid,
|
|
106
|
+
ErrorCode.FileNotFound,
|
|
107
|
+
ErrorCode.FileReadFailed,
|
|
108
|
+
ErrorCode.AuthRequired,
|
|
109
|
+
ErrorCode.NotionUnauthorized,
|
|
110
|
+
ErrorCode.NotionRestricted,
|
|
111
|
+
ErrorCode.NotionNotFound,
|
|
112
|
+
ErrorCode.NotionRateLimited,
|
|
113
|
+
ErrorCode.NotionValidation,
|
|
114
|
+
ErrorCode.NotionConflict,
|
|
115
|
+
ErrorCode.NotionTimeoutUncertain,
|
|
116
|
+
ErrorCode.NotionEnrollmentRequired,
|
|
117
|
+
ErrorCode.NotionBatchPartial,
|
|
118
|
+
ErrorCode.GateInvalidResponse,
|
|
119
|
+
ErrorCode.NetworkError,
|
|
120
|
+
ErrorCode.NetworkTimeout,
|
|
121
|
+
ErrorCode.ServiceUnavailable,
|
|
122
|
+
ErrorCode.ApiError,
|
|
123
|
+
];
|
|
124
|
+
const supportedPropertyTypes = new Set([
|
|
125
|
+
'title',
|
|
126
|
+
'rich_text',
|
|
127
|
+
'number',
|
|
128
|
+
'select',
|
|
129
|
+
'multi_select',
|
|
130
|
+
'status',
|
|
131
|
+
'date',
|
|
132
|
+
'people',
|
|
133
|
+
'files',
|
|
134
|
+
'checkbox',
|
|
135
|
+
'url',
|
|
136
|
+
'email',
|
|
137
|
+
'phone_number',
|
|
138
|
+
'relation',
|
|
139
|
+
'rollup',
|
|
140
|
+
'formula',
|
|
141
|
+
'created_time',
|
|
142
|
+
'created_by',
|
|
143
|
+
'last_edited_time',
|
|
144
|
+
'last_edited_by',
|
|
145
|
+
'unique_id',
|
|
146
|
+
'verification',
|
|
147
|
+
]);
|
|
148
|
+
const viewTypes = new Set(['table', 'board', 'list', 'calendar', 'timeline', 'gallery', 'form', 'chart', 'map']);
|
|
149
|
+
export function normalizeSchemaFlagArgv(argv) {
|
|
150
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
151
|
+
const token = argv[index];
|
|
152
|
+
if (token === '--schema')
|
|
153
|
+
argv[index] = '--schema-input';
|
|
154
|
+
else if (token?.startsWith('--schema='))
|
|
155
|
+
argv[index] = `--schema-input=${token.slice('--schema='.length)}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function emitRawOrReturn(result, raw) {
|
|
159
|
+
if (raw) {
|
|
160
|
+
await writeStdout(`${formatRawBody(result.raw)}\n`);
|
|
161
|
+
return result.raw;
|
|
162
|
+
}
|
|
163
|
+
return result.data;
|
|
164
|
+
}
|
|
165
|
+
export function formatRawBody(value) {
|
|
166
|
+
if (Array.isArray(value))
|
|
167
|
+
return value.map((item) => JSON.stringify(stripAuthQueryStrings(item), null, 2)).join('\n');
|
|
168
|
+
return JSON.stringify(stripAuthQueryStrings(value), null, 2);
|
|
169
|
+
}
|
|
170
|
+
export async function maybeDryRunWarning(flags, kind) {
|
|
171
|
+
if (!flags.quiet && !flags.json)
|
|
172
|
+
await writeStderr(`dry-run: no Notion write attempted for ${kind}\n`);
|
|
173
|
+
}
|
|
174
|
+
export async function parseJsonInput(value, label) {
|
|
175
|
+
const input = value === '-' ? await readStdin() : looksLikeJson(value) ? value : await readFileInput(value, label);
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(input);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
throw new EscliError(`${label} JSON is invalid: ${error instanceof Error ? error.message : String(error)}`, {
|
|
181
|
+
code: ErrorCode.JsonInvalid,
|
|
182
|
+
exitCode: ExitCodes.Usage,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export async function parseSchemaInput(value) {
|
|
187
|
+
const schema = await parseJsonInput(value, 'schema');
|
|
188
|
+
validateSchema(schema);
|
|
189
|
+
return schema;
|
|
190
|
+
}
|
|
191
|
+
export async function parseOptionalJsonInput(value, label) {
|
|
192
|
+
return value === undefined ? undefined : parseJsonInput(value, label);
|
|
193
|
+
}
|
|
194
|
+
export async function parseViewFileInput(value) {
|
|
195
|
+
if (!value)
|
|
196
|
+
return [];
|
|
197
|
+
const parsed = await parseJsonInput(value, 'view-file');
|
|
198
|
+
const views = Array.isArray(parsed) ? parsed : [parsed];
|
|
199
|
+
return views.map((view, index) => parseViewRecord(view, `view-file[${index}]`));
|
|
200
|
+
}
|
|
201
|
+
export function parseViewFlags(values) {
|
|
202
|
+
return (values ?? []).map((value) => {
|
|
203
|
+
const separator = value.indexOf(':');
|
|
204
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
205
|
+
throw new EscliError(`view must be <type:name>, got ${value}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
206
|
+
}
|
|
207
|
+
const type = value.slice(0, separator);
|
|
208
|
+
const name = value.slice(separator + 1);
|
|
209
|
+
assertViewType(type);
|
|
210
|
+
return { type, name };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export function parseViewType(value) {
|
|
214
|
+
assertViewType(value);
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
export function parseSortFlags(values) {
|
|
218
|
+
const sorts = (values ?? []).map((value) => {
|
|
219
|
+
const separator = value.lastIndexOf(':');
|
|
220
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
221
|
+
throw new EscliError(`sort must be <property:asc|property:desc>, got ${value}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
222
|
+
}
|
|
223
|
+
const property = value.slice(0, separator);
|
|
224
|
+
const direction = value.slice(separator + 1);
|
|
225
|
+
if (direction !== 'asc' && direction !== 'desc') {
|
|
226
|
+
throw new EscliError(`sort direction must be asc or desc, got ${direction}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
227
|
+
}
|
|
228
|
+
return { property, direction: direction === 'asc' ? 'ascending' : 'descending' };
|
|
229
|
+
});
|
|
230
|
+
return sorts.length > 0 ? sorts : undefined;
|
|
231
|
+
}
|
|
232
|
+
export function parsePosition(value) {
|
|
233
|
+
if (!value)
|
|
234
|
+
return undefined;
|
|
235
|
+
if (value === 'start' || value === 'end')
|
|
236
|
+
return value;
|
|
237
|
+
if (value.startsWith('after:') && value.length > 'after:'.length)
|
|
238
|
+
return { after_view_id: value.slice('after:'.length) };
|
|
239
|
+
throw new EscliError(`position must be start, end, or after:<view_id>, got ${value}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
240
|
+
}
|
|
241
|
+
export function viewLocation(flags) {
|
|
242
|
+
const provided = [flags['in-db'], flags['in-dashboard'], flags['linked-on']].filter(Boolean);
|
|
243
|
+
if (provided.length !== 1) {
|
|
244
|
+
throw new EscliError('choose exactly one view location: --in-db, --in-dashboard, or --linked-on', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
245
|
+
}
|
|
246
|
+
if (flags['in-db'])
|
|
247
|
+
return { database_id: flags['in-db'] };
|
|
248
|
+
if (flags['in-dashboard'])
|
|
249
|
+
return { dashboard_view_id: flags['in-dashboard'] };
|
|
250
|
+
return { page_id: flags['linked-on'] };
|
|
251
|
+
}
|
|
252
|
+
export function renderDbCreate(data) {
|
|
253
|
+
if (data.created === 'dry_run')
|
|
254
|
+
return renderDryRun(data.request);
|
|
255
|
+
const database = data.database;
|
|
256
|
+
const dataSource = data.data_source;
|
|
257
|
+
const rows = [
|
|
258
|
+
['created', data.created],
|
|
259
|
+
['database_id', database?.id],
|
|
260
|
+
['title', database?.title],
|
|
261
|
+
];
|
|
262
|
+
if (database?.parent_page_id)
|
|
263
|
+
rows.push(['parent_page_id', database.parent_page_id]);
|
|
264
|
+
else if (database?.parent_database_id)
|
|
265
|
+
rows.push(['parent_database_id', database.parent_database_id]);
|
|
266
|
+
else if (database?.parent)
|
|
267
|
+
rows.push(['parent', database.parent]);
|
|
268
|
+
rows.push(['data_source_id', dataSource?.id], ['data_source', dataSource?.name], ['properties', dataSource?.property_count ?? dataSource?.properties.length ?? 0]);
|
|
269
|
+
const sections = [renderKv(rows)];
|
|
270
|
+
if (dataSource?.properties.length)
|
|
271
|
+
sections.push(renderProperties(dataSource.properties));
|
|
272
|
+
if (data.views.length)
|
|
273
|
+
sections.push(renderViews(data.views));
|
|
274
|
+
sections.push(renderKv([['next', data.next]]));
|
|
275
|
+
if (data.warning)
|
|
276
|
+
sections.push(renderKv([['warning', data.warning]]));
|
|
277
|
+
return sections.join('\n\n');
|
|
278
|
+
}
|
|
279
|
+
export function renderDsCreate(data) {
|
|
280
|
+
if (data.created === 'dry_run')
|
|
281
|
+
return renderDryRun(data.request);
|
|
282
|
+
const dataSource = data.data_source;
|
|
283
|
+
const sections = [renderKv([
|
|
284
|
+
['created', data.created],
|
|
285
|
+
['database_id', data.database_id],
|
|
286
|
+
['data_source_id', dataSource?.id],
|
|
287
|
+
['name', dataSource?.name],
|
|
288
|
+
['properties', dataSource?.property_count ?? dataSource?.properties.length ?? 0],
|
|
289
|
+
])];
|
|
290
|
+
if (dataSource?.properties.length)
|
|
291
|
+
sections.push(renderProperties(dataSource.properties));
|
|
292
|
+
if (data.views.length)
|
|
293
|
+
sections.push(renderViews(data.views));
|
|
294
|
+
sections.push(renderKv([['next', data.next]]));
|
|
295
|
+
return sections.join('\n\n');
|
|
296
|
+
}
|
|
297
|
+
export function renderViewCreate(data) {
|
|
298
|
+
if (data.created === 'dry_run')
|
|
299
|
+
return renderDryRun(data.request);
|
|
300
|
+
const view = data.view;
|
|
301
|
+
return renderKv([
|
|
302
|
+
['created', data.created],
|
|
303
|
+
['view_id', view?.id],
|
|
304
|
+
['name', view?.name],
|
|
305
|
+
['type', view?.type],
|
|
306
|
+
['data_source_id', view?.data_source_id],
|
|
307
|
+
['location', view ? renderLocation(view.location) : undefined],
|
|
308
|
+
['filter', data.query_shape.filter],
|
|
309
|
+
['sorts', data.query_shape.sorts.join(', ')],
|
|
310
|
+
['next', data.next],
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
export function normalizeDbCreateResponse(value, title, parent, dataSourceName) {
|
|
314
|
+
const root = recordValue(value);
|
|
315
|
+
if (isDbCreateData(root))
|
|
316
|
+
return root;
|
|
317
|
+
const dataSource = firstRecord(root?.data_sources) ?? findRawDataSource(value);
|
|
318
|
+
const databaseId = stringValue(root?.id) ?? stringValue(recordValue(recordValue(dataSource)?.parent)?.database_id) ?? '';
|
|
319
|
+
const dataSourceId = stringValue(recordValue(dataSource)?.id) ?? '';
|
|
320
|
+
return {
|
|
321
|
+
created: 'database',
|
|
322
|
+
database: {
|
|
323
|
+
id: databaseId,
|
|
324
|
+
title: extractTitle(root?.title) ?? title,
|
|
325
|
+
...parentField(root?.parent, parent),
|
|
326
|
+
},
|
|
327
|
+
data_source: {
|
|
328
|
+
id: dataSourceId,
|
|
329
|
+
name: stringValue(recordValue(dataSource)?.name) ?? dataSourceName,
|
|
330
|
+
properties: extractProperties(recordValue(dataSource)?.properties ?? root?.properties),
|
|
331
|
+
property_count: extractProperties(recordValue(dataSource)?.properties ?? root?.properties).length,
|
|
332
|
+
},
|
|
333
|
+
views: extractViews(value),
|
|
334
|
+
next: `escli notion page create --data-source ${dataSourceId} --props <file>`,
|
|
335
|
+
warning: databaseId && dataSourceId && databaseId !== dataSourceId ? 'database_id and data_source_id are different; rows use data_source_id.' : undefined,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export function normalizeDsCreateResponse(value, databaseId, name) {
|
|
339
|
+
const root = recordValue(value);
|
|
340
|
+
if (isDsCreateData(root))
|
|
341
|
+
return root;
|
|
342
|
+
const dataSource = root?.object === 'data_source' ? root : findRawDataSource(value);
|
|
343
|
+
const dataSourceId = stringValue(dataSource?.id) ?? '';
|
|
344
|
+
const properties = extractProperties(dataSource?.properties);
|
|
345
|
+
return {
|
|
346
|
+
created: 'data_source',
|
|
347
|
+
database_id: stringValue(recordValue(dataSource?.parent)?.database_id) ?? databaseId,
|
|
348
|
+
data_source: { id: dataSourceId, name: stringValue(dataSource?.name) ?? name, properties, property_count: properties.length },
|
|
349
|
+
views: extractViews(value),
|
|
350
|
+
next: `escli notion page create --data-source ${dataSourceId} --props <file>`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export function normalizeViewCreateResponse(value, fallback) {
|
|
354
|
+
const root = recordValue(value);
|
|
355
|
+
if (isViewCreateData(root))
|
|
356
|
+
return root;
|
|
357
|
+
const view = findRawView(value) ?? root;
|
|
358
|
+
const viewId = stringValue(view?.id) ?? '';
|
|
359
|
+
const sorts = extractSortLabels(view?.sorts) ?? (fallback.sorts ?? []).map((sort) => `${sort.property} ${sort.direction === 'ascending' ? 'asc' : 'desc'}`);
|
|
360
|
+
return {
|
|
361
|
+
created: 'view',
|
|
362
|
+
view: {
|
|
363
|
+
id: viewId,
|
|
364
|
+
name: stringValue(view?.name) ?? fallback.name,
|
|
365
|
+
type: stringValue(view?.type) ?? fallback.type,
|
|
366
|
+
data_source_id: stringValue(view?.data_source_id) ?? fallback.data_source_id,
|
|
367
|
+
location: locationFromRaw(view) ?? fallback.location,
|
|
368
|
+
},
|
|
369
|
+
query_shape: { filter: summarizeFilter(view?.filter ?? fallback.filter), sorts },
|
|
370
|
+
next: `escli notion view query ${viewId}`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
export function dryRunData(kind, request) {
|
|
374
|
+
return { created: 'dry_run', views: [], next: `dry-run: rerun without --dry-run to create ${kind}`, request };
|
|
375
|
+
}
|
|
376
|
+
export function notionMetaEnvelope(data) {
|
|
377
|
+
return { ...data, next_cursor: null, notion_version: NOTION_VERSION };
|
|
378
|
+
}
|
|
379
|
+
function renderProperties(properties) {
|
|
380
|
+
return renderTable({ header: ['property', 'id', 'type'], rows: properties.map((property) => [property.name, property.id, property.type]) });
|
|
381
|
+
}
|
|
382
|
+
function renderViews(views) {
|
|
383
|
+
return renderTable({ header: ['view', 'id', 'type'], rows: views.map((view) => [view.name, view.id, view.type]) });
|
|
384
|
+
}
|
|
385
|
+
function renderDryRun(request) {
|
|
386
|
+
return `${renderKv([['created', 'dry_run'], ['notion_version', NOTION_VERSION], ['write_attempted', false]])}\n\n${JSON.stringify(request, null, 2)}`;
|
|
387
|
+
}
|
|
388
|
+
function renderLocation(location) {
|
|
389
|
+
if ('database_id' in location)
|
|
390
|
+
return `database ${location.database_id}`;
|
|
391
|
+
if ('dashboard_view_id' in location)
|
|
392
|
+
return `dashboard ${location.dashboard_view_id}`;
|
|
393
|
+
return `page ${location.page_id}`;
|
|
394
|
+
}
|
|
395
|
+
function validateSchema(schema) {
|
|
396
|
+
const properties = recordValue(recordValue(schema)?.properties ?? schema);
|
|
397
|
+
if (!properties)
|
|
398
|
+
throw new EscliError('schema must be a JSON object with properties', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
399
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
400
|
+
const type = stringValue(recordValue(property)?.type) ?? Object.keys(recordValue(property) ?? {})[0];
|
|
401
|
+
if (!type || !supportedPropertyTypes.has(type)) {
|
|
402
|
+
throw new EscliError(`unsupported schema property type for ${name}: ${type ?? 'missing'}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function parseViewRecord(value, label) {
|
|
407
|
+
const record = recordValue(value);
|
|
408
|
+
if (!record)
|
|
409
|
+
throw new EscliError(`${label} must be an object`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
410
|
+
const type = stringValue(record.type);
|
|
411
|
+
const name = stringValue(record.name);
|
|
412
|
+
if (!type || !name)
|
|
413
|
+
throw new EscliError(`${label} must include type and name`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
414
|
+
assertViewType(type);
|
|
415
|
+
return { ...record, type, name };
|
|
416
|
+
}
|
|
417
|
+
function assertViewType(value) {
|
|
418
|
+
if (!viewTypes.has(value))
|
|
419
|
+
throw new EscliError(`unsupported view type: ${value}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
420
|
+
}
|
|
421
|
+
async function readStdin() {
|
|
422
|
+
const chunks = [];
|
|
423
|
+
for await (const chunk of process.stdin)
|
|
424
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
425
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
426
|
+
}
|
|
427
|
+
async function readFileInput(path, label) {
|
|
428
|
+
try {
|
|
429
|
+
return await readFile(path, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
throw new EscliError(`${label} file not found or unreadable: ${path}`, { code: ErrorCode.FileReadFailed, exitCode: ExitCodes.NotFound, details: error instanceof Error ? error.message : error });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function looksLikeJson(value) {
|
|
436
|
+
const trimmed = value.trimStart();
|
|
437
|
+
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
|
438
|
+
}
|
|
439
|
+
function stripAuthQueryStrings(value) {
|
|
440
|
+
if (Array.isArray(value))
|
|
441
|
+
return value.map(stripAuthQueryStrings);
|
|
442
|
+
const record = recordValue(value);
|
|
443
|
+
if (!record)
|
|
444
|
+
return value;
|
|
445
|
+
return Object.fromEntries(Object.entries(record).map(([key, nested]) => [key, key === 'upload_url' || key === 'complete_url' ? stripQueryString(nested) : stripAuthQueryStrings(nested)]));
|
|
446
|
+
}
|
|
447
|
+
function stripQueryString(value) {
|
|
448
|
+
return typeof value === 'string' ? value.replace(/\?.*$/u, '') : value;
|
|
449
|
+
}
|
|
450
|
+
function isDbCreateData(value) {
|
|
451
|
+
return value?.created === 'database' && Boolean(recordValue(value.database));
|
|
452
|
+
}
|
|
453
|
+
function isDsCreateData(value) {
|
|
454
|
+
return value?.created === 'data_source' && typeof value.database_id === 'string';
|
|
455
|
+
}
|
|
456
|
+
function isViewCreateData(value) {
|
|
457
|
+
return value?.created === 'view' && Boolean(recordValue(value.view));
|
|
458
|
+
}
|
|
459
|
+
function extractProperties(value) {
|
|
460
|
+
const properties = recordValue(value);
|
|
461
|
+
if (!properties)
|
|
462
|
+
return [];
|
|
463
|
+
return Object.entries(properties).map(([name, property]) => {
|
|
464
|
+
const record = recordValue(property) ?? {};
|
|
465
|
+
return { name: stringValue(record.name) ?? name, id: stringValue(record.id) ?? '', type: stringValue(record.type) ?? '' };
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function extractViews(value) {
|
|
469
|
+
const values = Array.isArray(value) ? value : Array.isArray(recordValue(value)?.views) ? recordValue(value)?.views : [];
|
|
470
|
+
return values.map(recordValue).filter((view) => view?.object === 'view' || (typeof view?.id === 'string' && typeof view?.type === 'string' && typeof view?.name === 'string')).map((view) => ({ id: stringValue(view?.id) ?? '', name: stringValue(view?.name) ?? '', type: stringValue(view?.type) ?? '' }));
|
|
471
|
+
}
|
|
472
|
+
function findRawDataSource(value) {
|
|
473
|
+
const values = Array.isArray(value) ? value : [];
|
|
474
|
+
return values.map(recordValue).find((record) => record?.object === 'data_source');
|
|
475
|
+
}
|
|
476
|
+
function findRawView(value) {
|
|
477
|
+
const values = Array.isArray(value) ? value : [];
|
|
478
|
+
return values.map(recordValue).find((record) => record?.object === 'view');
|
|
479
|
+
}
|
|
480
|
+
function locationFromRaw(value) {
|
|
481
|
+
const databaseId = stringValue(value?.database_id);
|
|
482
|
+
if (databaseId)
|
|
483
|
+
return { database_id: databaseId };
|
|
484
|
+
const dashboardId = stringValue(value?.dashboard_view_id);
|
|
485
|
+
if (dashboardId)
|
|
486
|
+
return { dashboard_view_id: dashboardId };
|
|
487
|
+
const pageId = stringValue(value?.page_id);
|
|
488
|
+
if (pageId)
|
|
489
|
+
return { page_id: pageId };
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
function parentField(parent, fallback) {
|
|
493
|
+
const record = recordValue(parent);
|
|
494
|
+
const pageId = stringValue(record?.page_id);
|
|
495
|
+
if (pageId)
|
|
496
|
+
return { parent_page_id: pageId };
|
|
497
|
+
const databaseId = stringValue(record?.database_id);
|
|
498
|
+
if (databaseId)
|
|
499
|
+
return { parent_database_id: databaseId };
|
|
500
|
+
return { parent_page_id: fallback };
|
|
501
|
+
}
|
|
502
|
+
function extractTitle(value) {
|
|
503
|
+
if (typeof value === 'string')
|
|
504
|
+
return value;
|
|
505
|
+
if (Array.isArray(value))
|
|
506
|
+
return value.map((part) => stringValue(recordValue(part)?.plain_text) ?? stringValue(recordValue(recordValue(part)?.text)?.content) ?? '').join('') || undefined;
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
function extractSortLabels(value) {
|
|
510
|
+
if (!Array.isArray(value))
|
|
511
|
+
return undefined;
|
|
512
|
+
return value.map((sort) => {
|
|
513
|
+
const record = recordValue(sort) ?? {};
|
|
514
|
+
const property = stringValue(record.property) ?? '';
|
|
515
|
+
const direction = stringValue(record.direction) === 'ascending' ? 'asc' : stringValue(record.direction) === 'descending' ? 'desc' : stringValue(record.direction) ?? '';
|
|
516
|
+
return `${property} ${direction}`.trim();
|
|
517
|
+
}).filter(Boolean);
|
|
518
|
+
}
|
|
519
|
+
function summarizeFilter(value) {
|
|
520
|
+
if (!value)
|
|
521
|
+
return undefined;
|
|
522
|
+
const count = countRules(value);
|
|
523
|
+
const firstProperty = firstFilterProperty(value);
|
|
524
|
+
return firstProperty ? `${count} ${firstProperty} rule${count === 1 ? '' : 's'}` : `${count} rule${count === 1 ? '' : 's'}`;
|
|
525
|
+
}
|
|
526
|
+
function countRules(value) {
|
|
527
|
+
if (Array.isArray(value))
|
|
528
|
+
return value.reduce((sum, item) => sum + countRules(item), 0);
|
|
529
|
+
const record = recordValue(value);
|
|
530
|
+
if (!record)
|
|
531
|
+
return 0;
|
|
532
|
+
if (Array.isArray(record.and))
|
|
533
|
+
return countRules(record.and);
|
|
534
|
+
if (Array.isArray(record.or))
|
|
535
|
+
return countRules(record.or);
|
|
536
|
+
return stringValue(record.property) ? 1 : Object.values(record).reduce((sum, item) => sum + countRules(item), 0);
|
|
537
|
+
}
|
|
538
|
+
function firstFilterProperty(value) {
|
|
539
|
+
if (Array.isArray(value))
|
|
540
|
+
return value.map(firstFilterProperty).find(Boolean);
|
|
541
|
+
const record = recordValue(value);
|
|
542
|
+
if (!record)
|
|
543
|
+
return undefined;
|
|
544
|
+
return stringValue(record.property) ?? firstFilterProperty(record.and) ?? firstFilterProperty(record.or);
|
|
545
|
+
}
|
|
546
|
+
function firstRecord(value) {
|
|
547
|
+
return Array.isArray(value) ? recordValue(value[0]) : undefined;
|
|
548
|
+
}
|
|
549
|
+
function recordValue(value) {
|
|
550
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
551
|
+
}
|
|
552
|
+
function stringValue(value) {
|
|
553
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
554
|
+
}
|
|
555
|
+
//# sourceMappingURL=create.js.map
|