Skip to content

Commit e0a41d9

Browse files
committed
feat: add support for created by and last edited by
1 parent 3e0c3e7 commit e0a41d9

File tree

7 files changed

+273
-17
lines changed

7 files changed

+273
-17
lines changed

source/client.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* -------------------------------------------------------------------------
1414
*/
1515

16-
import { Client } from '@notionhq/client';
16+
import { APIErrorCode, Client, isNotionClientError } from '@notionhq/client';
1717
import { caching } from 'cache-manager';
1818
import { dump } from 'js-yaml';
1919

@@ -27,18 +27,19 @@ import {
2727
normalizeProperties,
2828
} from '#property';
2929

30+
import type { Cache } from 'cache-manager';
31+
3032
import type {
3133
Block,
3234
Database,
3335
NotionAPIDatabase,
3436
NotionAPIList,
3537
NotionAPIPage,
3638
NotionAPITitle,
39+
NotionAPIUser,
3740
Page,
3841
} from './types';
3942

40-
import type { Cache } from 'cache-manager';
41-
4243
export interface NotionTTL {
4344
/** the number of seconds in which a database metadata will be cached */
4445
databaseMeta: number;
@@ -142,7 +143,13 @@ export class Notion {
142143
object: 'database',
143144
parent: database.parent,
144145
title: getPropertyContentFromRichText(database.title),
145-
metadata: getMetadata(database),
146+
metadata: getMetadata({
147+
...database,
148+
/* eslint-disable @typescript-eslint/naming-convention */
149+
created_by: await this.getUser(database.created_by.id),
150+
last_edited_by: await this.getUser(database.last_edited_by.id),
151+
/* eslint-enable @typescript-eslint/naming-convention */
152+
}),
146153
pages: normalizedPages,
147154
};
148155
}
@@ -168,6 +175,32 @@ export class Notion {
168175
return this.normalizePageAndCache(page);
169176
}
170177

178+
/**
179+
* get user detail with cache
180+
* @param id the uuid of a Notion user to be queried
181+
* @returns user object returned from Notion's API
182+
*/
183+
public async getUser(id: string): Promise<NotionAPIUser | null> {
184+
try {
185+
return await this.cache.wrap(
186+
`user:${id}`,
187+
/* eslint-disable @typescript-eslint/naming-convention */
188+
async () => this.client.users.retrieve({ user_id: id }),
189+
/* eslint-enable @typescript-eslint/naming-convention */
190+
);
191+
} catch (error) {
192+
if (
193+
isNotionClientError(error) &&
194+
error.code === APIErrorCode.RestrictedResource
195+
) {
196+
// NOTE: not throwing an error here because users may still want other data
197+
return null;
198+
} else {
199+
throw error;
200+
}
201+
}
202+
}
203+
171204
/**
172205
* get all block related to a collection
173206
* @param id the uuid of the collection, either a database or page or a parent block
@@ -193,7 +226,7 @@ export class Notion {
193226
...(block.has_children
194227
? { has_children: true, children: await this.getBlocks(block.id) }
195228
: { has_children: false }),
196-
/* eslint-enable */
229+
/* eslint-enable @typescript-eslint/naming-convention */
197230
}),
198231
),
199232
);
@@ -213,7 +246,13 @@ export class Notion {
213246
)[0];
214247
const title = getPropertyContentFromRichText(titleProperty.title);
215248

216-
const metadata = getMetadata(page);
249+
const metadata = getMetadata({
250+
...page,
251+
/* eslint-disable @typescript-eslint/naming-convention */
252+
created_by: await this.getUser(page.created_by.id),
253+
last_edited_by: await this.getUser(page.last_edited_by.id),
254+
/* eslint-enable @typescript-eslint/naming-convention */
255+
});
217256

218257
return {
219258
id: page.id,

source/metadata.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,74 @@
1313
* -------------------------------------------------------------------------
1414
*/
1515

16-
import { getPropertyContentFromFile } from '#property';
16+
import {
17+
getPropertyContentFromFile,
18+
getPropertyContentFromUser,
19+
} from '#property';
1720

18-
import type { Metadata, NotionAPIDatabase, NotionAPIPage } from '#types';
21+
import type {
22+
EntityWithUserDetail,
23+
Metadata,
24+
NotionAPIDatabase,
25+
NotionAPIPage,
26+
NotionAPIUser,
27+
} from '#types';
1928

2029
/**
2130
* get common properties from a page or database, except for the title
2231
* @param entity the page or database object returned from Notion API
2332
* @returns common properties
2433
*/
25-
export function getMetadata<E extends NotionAPIPage | NotionAPIDatabase>(
26-
entity: E,
27-
): Metadata {
34+
export function getMetadata<
35+
E extends EntityWithUserDetail<NotionAPIPage | NotionAPIDatabase>,
36+
>(entity: E): Metadata {
2837
const { last_edited_time: lastEditedTime, url } = entity;
2938
const { created_time: createdTime } = entity;
3039

31-
const variable = { url, lastEditedTime };
32-
const invariant = { createdTime };
40+
const variable = {
41+
url,
42+
...getCommonPersonMetadata('lastEditedBy', entity.last_edited_by),
43+
lastEditedTime,
44+
};
45+
const invariant = {
46+
...getCommonPersonMetadata('createdBy', entity.created_by),
47+
createdTime,
48+
};
3349
const visual = getCommonVisualMetadata(entity);
3450

3551
return { ...variable, ...invariant, ...visual };
3652
}
3753

54+
/**
55+
* get common visual properties from a page or database
56+
* @param prefix the prefix attached to the property name
57+
* @param user the user object returned from Notion API
58+
* @returns common properties
59+
*/
60+
export function getCommonPersonMetadata<P extends string>(
61+
prefix: P,
62+
user: NotionAPIUser | null,
63+
): {
64+
[K in `${P}${'Avatar' | 'Email' | 'Name'}`]: string | null;
65+
} {
66+
const properties = getPropertyContentFromUser(user);
67+
68+
return {
69+
[`${prefix}Avatar`]: properties?.avatar ?? null,
70+
[`${prefix}Email`]: properties?.email ?? null,
71+
[`${prefix}Name`]: properties?.name ?? null,
72+
} as {
73+
[K in `${P}${'Avatar' | 'Email' | 'Name'}`]: string | null;
74+
};
75+
}
76+
3877
/**
3978
* get common visual properties such as cover and icon from a page or database
4079
* @param entity the page or database object returned from Notion API
4180
* @returns common properties
4281
*/
4382
export function getCommonVisualMetadata<
44-
E extends NotionAPIPage | NotionAPIDatabase,
83+
E extends EntityWithUserDetail<NotionAPIPage | NotionAPIDatabase>,
4584
>(
4685
entity: E,
4786
): {

source/property.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ export function getPropertyContent<
5353
formula: boolean | number | string | null;
5454
rollup: number | string | null;
5555
created_time: string;
56+
created_by: Person | null;
5657
last_edited_time: string;
58+
last_edited_by: Person | null;
5759
/* eslint-enable @typescript-eslint/naming-convention */
5860
}[T];
5961
export function getPropertyContent(
@@ -94,8 +96,12 @@ export function getPropertyContent(
9496
return getPropertyContentFromFormula(property.formula);
9597
case 'rollup':
9698
return getPropertyContentFromRollup(property.rollup);
99+
case 'created_by':
100+
return getPropertyContentFromUser(property.created_by);
97101
case 'created_time':
98102
return property.created_time;
103+
case 'last_edited_by':
104+
return getPropertyContentFromUser(property.last_edited_by);
99105
case 'last_edited_time':
100106
return property.last_edited_time;
101107
// @ts-expect-error Notion has unsupported property type in the past and also maybe in future

source/types/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ export type Block = NotionAPIBlock &
8383

8484
export type Metadata = {
8585
url: string;
86+
createdByAvatar: string | null;
87+
createdByEmail: string | null;
88+
createdByName: string | null;
8689
createdTime: string;
90+
lastEditedByAvatar: string | null;
91+
lastEditedByEmail: string | null;
92+
lastEditedByName: string | null;
8793
lastEditedTime: string;
8894
coverImage: string | null;
8995
iconEmoji: string | null;

spec/client.spec.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515

1616
import assert from 'assert';
1717

18-
import { Notion, getCommonMetadata } from '#client';
19-
import { mockDatabase, mockPage } from './mock';
20-
import { NotionAPIPage } from '#types';
18+
import { APIErrorCode } from '@notionhq/client';
19+
20+
import { Notion } from '#client';
21+
import { mockDatabase, mockPage, mockUser } from './mock';
2122

2223
describe('cl:Notion', () => {
2324
const client = new Notion({ token: 'token' });
@@ -70,7 +71,13 @@ describe('cl:Notion', () => {
7071
---
7172
title: 'Text'
7273
url: 'https://www.notion.so/workspace/page'
74+
lastEditedByAvatar: 'url'
75+
lastEditedByEmail: 'email'
76+
lastEditedByName: 'Name'
7377
lastEditedTime: '2020-01-01T00:00:00Z'
78+
createdByAvatar: 'url'
79+
createdByEmail: 'email'
80+
createdByName: 'Name'
7481
createdTime: '2020-01-01T00:00:00Z'
7582
coverImage: 'https://www.notion.so/cover.png'
7683
iconEmoji: '📚'
@@ -97,4 +104,61 @@ page-block1-block0
97104
expect(page.blocks.length).toEqual(BLOCKS_PER_PAGE);
98105
});
99106
});
107+
108+
describe('fn:getUser', () => {
109+
it('return user information as detail as possible', async () => {
110+
const userID = 'user_with_full_detail';
111+
mockUser({ userID });
112+
113+
const user = await client.getUser(userID);
114+
115+
expect(user).toEqual({
116+
object: 'user',
117+
type: 'person',
118+
id: userID,
119+
name: 'Name',
120+
avatar_url: 'url',
121+
person: {
122+
email: 'email',
123+
},
124+
});
125+
});
126+
127+
it('return null for API token without user read capability', async () => {
128+
const userID = 'inaccessible_user';
129+
mockUser(
130+
{ userID },
131+
{
132+
error: {
133+
type: 'code',
134+
status: 403,
135+
code: APIErrorCode.RestrictedResource,
136+
},
137+
},
138+
);
139+
140+
const user = await client.getUser(userID);
141+
142+
expect(user).toEqual(null);
143+
});
144+
145+
it('throw an error if there is any unexpected http error other than 403', async () => {
146+
const userID = 'http_error';
147+
mockUser(
148+
{ userID },
149+
{
150+
error: { type: 'code', status: 400, code: APIErrorCode.InvalidJSON },
151+
},
152+
);
153+
154+
await expect(async () => client.getUser(userID)).rejects.toThrowError();
155+
});
156+
157+
it('throw any non http error', async () => {
158+
const userID = 'network_error';
159+
mockUser({ userID }, { error: { type: 'network' } });
160+
161+
await expect(async () => client.getUser(userID)).rejects.toThrowError();
162+
});
163+
});
100164
});

0 commit comments

Comments
 (0)