This repository has been archived by the owner on Feb 7, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathnotion.mjs
367 lines (354 loc) · 12.8 KB
/
notion.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
/**
* notion-enhancer: api
* (c) 2021 dragonwocky <[email protected]> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
/**
* a basic wrapper around notion's content apis
* @namespace notion
*/
import { web, fs, fmt } from './index.mjs';
const standardiseUUID = (uuid) => {
if (uuid?.length === 32 && !uuid.includes('-')) {
uuid = uuid.replace(
/([\d\w]{8})([\d\w]{4})([\d\w]{4})([\d\w]{4})([\d\w]{12})/,
'$1-$2-$3-$4-$5'
);
}
return uuid;
};
/**
* unofficial content api: get a block by id
* (requires user to be signed in or content to be public).
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {string} id - uuidv4 record id
* @param {string=} table - record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @returns {Promise<object>} record data. type definitions can be found here:
* https://github.com/NotionX/react-notion-x/tree/master/packages/notion-types/src
*/
export const get = async (id, table = 'block') => {
id = standardiseUUID(id);
const json = await fs.getJSON('https://www.notion.so/api/v3/getRecordValues', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests: [{ table, id }] }),
method: 'POST',
});
return json?.results?.[0]?.value || json;
};
/**
* get the id of the current user (requires user to be signed in)
* @returns {string} uuidv4 user id
*/
export const getUserID = () =>
JSON.parse(localStorage['LRU:KeyValueStore2:current-user-id'] || {}).value;
/**
* get the id of the currently open page
* @returns {string} uuidv4 page id
*/
export const getPageID = () =>
standardiseUUID(
web.queryParams().get('p') || location.pathname.split(/(-|\/)/g).reverse()[0]
);
let _spaceID;
/**
* get the id of the currently open workspace (requires user to be signed in)
* @returns {string} uuidv4 space id
*/
export const getSpaceID = async () => {
if (!_spaceID) _spaceID = (await get(getPageID())).space_id;
return _spaceID;
};
/**
* unofficial content api: search all blocks in a space
* (requires user to be signed in or content to be public).
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {string=} query - query to search blocks in the space for
* @param {number=} limit - the max number of results to return (default: 20)
* @param {string=} spaceID - uuidv4 workspace id
* @returns {object} the number of total results, the list of matches, and related record values.
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/api.ts
*/
export const search = async (query = '', limit = 20, spaceID = getSpaceID()) => {
spaceID = standardiseUUID(await spaceID);
const json = await fs.getJSON('https://www.notion.so/api/v3/search', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'BlocksInSpace',
query,
spaceId: spaceID,
limit,
filters: {
isDeletedOnly: false,
excludeTemplates: false,
isNavigableOnly: false,
requireEditPermissions: false,
ancestors: [],
createdBy: [],
editedBy: [],
lastEditedTime: {},
createdTime: {},
},
sort: 'Relevance',
source: 'quick_find',
}),
method: 'POST',
});
return json;
};
/**
* unofficial content api: update a property/the content of an existing record
* (requires user to be signed in or content to be public).
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
* to be unable to parse and render content properly and throw errors.
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {object} pointer - the record being updated
* @param {object} recordValue - the new raw data values to set to the record.
* for examples, use notion.get to fetch an existing block record.
* to use this to update content, set pointer.path to ['properties', 'title]
* and recordValue to an array of rich text segments. a segment is an array
* where the first value is the displayed text and the second value
* is an array of decorations. a decoration is an array where the first value
* is a modifier and the second value specifies it. e.g.
* [
* ['bold text', [['b']]],
* [' '],
* ['an italicised link', [['i'], ['a', 'https://github.com']]],
* [' '],
* ['highlighted text', [['h', 'pink_background']]],
* ]
* more examples can be creating a block with the desired content/formatting,
* then find the value of blockRecord.properties.title using notion.get.
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/core.ts
* @param {string} pointer.recordID - uuidv4 record id
* @param {string=} pointer.recordTable - record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @param {string=} pointer.property - the record property to update.
* for record content, it will be the default: 'title'.
* for page properties, it will be the property id (the key used in pageRecord.properties).
* other possible values are unknown/untested
* @param {string=} pointer.spaceID - uuidv4 workspace id
* @param {string=} pointer.path - the path to the key to be set within the record
* (default: [], the root of the record's values)
* @returns {boolean|object} true if success, else an error object
*/
export const set = async (
{ recordID, recordTable = 'block', spaceID = getSpaceID(), path = [] },
recordValue = {}
) => {
spaceID = standardiseUUID(await spaceID);
recordID = standardiseUUID(recordID);
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId: fmt.uuidv4(),
transactions: [
{
id: fmt.uuidv4(),
spaceId: spaceID,
operations: [
{
pointer: {
table: recordTable,
id: recordID,
spaceId: spaceID,
},
path,
command: path.length ? 'set' : 'update',
args: recordValue,
},
],
},
],
}),
method: 'POST',
});
return json.errorId ? json : true;
};
/**
* unofficial content api: create and add a new block to a page
* (requires user to be signed in or content to be public).
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
* to be unable to parse and render content properly and throw errors.
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {object} insert - the new record.
* @param {object} pointer - where to insert the new block
* for examples, use notion.get to fetch an existing block record.
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/block.ts
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @param {object=} insert.recordValue - the new raw data values to set to the record.
* @param {object=} insert.recordTable - record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @param {string=} pointer.prepend - insert before pointer.siblingID. if false, will be appended after
* @param {string=} pointer.siblingID - uuidv4 sibling id. if unset, the record will be
* inserted at the end of the page start (or the start if pointer.prepend is true)
* @param {string=} pointer.parentID - uuidv4 parent id
* @param {string=} pointer.parentTable - parent record type (default: 'block').
* @param {string=} pointer.spaceID - uuidv4 space id
* @param {string=} pointer.userID - uuidv4 user id
* instead of the end
* @returns {string|object} error object or uuidv4 of the new record
*/
export const create = async (
{ recordValue = {}, recordTable = 'block' } = {},
{
prepend = false,
siblingID = undefined,
parentID = getPageID(),
parentTable = 'block',
spaceID = getSpaceID(),
userID = getUserID(),
} = {}
) => {
spaceID = standardiseUUID(await spaceID);
parentID = standardiseUUID(parentID);
siblingID = standardiseUUID(siblingID);
const recordID = standardiseUUID(recordValue?.id ?? fmt.uuidv4()),
path = [],
args = {
type: 'text',
id: recordID,
version: 0,
created_time: new Date().getTime(),
last_edited_time: new Date().getTime(),
parent_id: parentID,
parent_table: parentTable,
alive: true,
created_by_table: 'notion_user',
created_by_id: userID,
last_edited_by_table: 'notion_user',
last_edited_by_id: userID,
space_id: spaceID,
permissions: [{ type: 'user_permission', role: 'editor', user_id: userID }],
};
if (parentTable === 'space') {
parentID = spaceID;
args.parent_id = spaceID;
path.push('pages');
args.type = 'page';
} else if (parentTable === 'collection_view') {
path.push('page_sort');
args.type = 'page';
} else {
path.push('content');
}
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId: fmt.uuidv4(),
transactions: [
{
id: fmt.uuidv4(),
spaceId: spaceID,
operations: [
{
pointer: {
table: parentTable,
id: parentID,
spaceId: spaceID,
},
path,
command: prepend ? 'listBefore' : 'listAfter',
args: {
...(siblingID ? { after: siblingID } : {}),
id: recordID,
},
},
{
pointer: {
table: recordTable,
id: recordID,
spaceId: spaceID,
},
path: [],
command: 'set',
args: {
...args,
...recordValue,
},
},
],
},
],
}),
method: 'POST',
});
return json.errorId ? json : recordID;
};
/**
* unofficial content api: upload a file to notion's aws servers
* (requires user to be signed in or content to be public).
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
* to be unable to parse and render content properly and throw errors.
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {File} file - the file to upload
* @param {object=} pointer - where the file should be accessible from
* @param {string=} pointer.pageID - uuidv4 page id
* @param {string=} pointer.spaceID - uuidv4 space id
* @returns {string|object} error object or the url of the uploaded file
*/
export const upload = async (file, { pageID = getPageID(), spaceID = getSpaceID() } = {}) => {
spaceID = standardiseUUID(await spaceID);
pageID = standardiseUUID(pageID);
const json = await fs.getJSON('https://www.notion.so/api/v3/getUploadFileUrl', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
bucket: 'secure',
name: file.name,
contentType: file.type,
record: {
table: 'block',
id: pageID,
spaceId: spaceID,
},
}),
});
if (json.errorId) return json;
fetch(json.signedPutUrl, {
method: 'PUT',
headers: { 'content-type': file.type },
body: file,
});
return json.url;
};
/**
* redirect through notion to a resource's signed aws url for display outside of notion
* (requires user to be signed in or content to be public)
* @param src source url for file
* @param {string} recordID uuidv4 record/block/file id
* @param {string=} recordTable record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @returns {string} url signed if necessary, else string as-is
*/
export const sign = (src, recordID, recordTable = 'block') => {
if (src.startsWith('/')) src = `https://notion.so${src}`;
if (src.includes('secure.notion-static.com')) {
src = new URL(src);
src = `https://www.notion.so/signed/${encodeURIComponent(
src.origin + src.pathname
)}?table=${recordTable}&id=${recordID}`;
}
return src;
};