@eightyfourthousand/data-access 2026.3.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/.babelrc +12 -0
- package/.eslintrc.json +18 -0
- package/README.md +7 -0
- package/jest.config.ts +10 -0
- package/package.json +30 -0
- package/project.json +21 -0
- package/src/index.ts +18 -0
- package/src/lib/auth.ts +203 -0
- package/src/lib/bibliography.ts +57 -0
- package/src/lib/canon.ts +111 -0
- package/src/lib/client-browser.ts +12 -0
- package/src/lib/client-server.ts +30 -0
- package/src/lib/client-ssr.ts +24 -0
- package/src/lib/client-token.ts +24 -0
- package/src/lib/feedback.ts +71 -0
- package/src/lib/folio.ts +43 -0
- package/src/lib/glossary.ts +147 -0
- package/src/lib/imprint.ts +42 -0
- package/src/lib/library.ts +209 -0
- package/src/lib/local-storage.ts +33 -0
- package/src/lib/lookup-entity.ts +122 -0
- package/src/lib/panel-url.ts +43 -0
- package/src/lib/passage.ts +122 -0
- package/src/lib/projects.ts +107 -0
- package/src/lib/proxy.ts +68 -0
- package/src/lib/publications.ts +213 -0
- package/src/lib/storage.ts +23 -0
- package/src/lib/types/alignment.ts +32 -0
- package/src/lib/types/annotation/abbreviation.ts +30 -0
- package/src/lib/types/annotation/annotation-type.ts +219 -0
- package/src/lib/types/annotation/annotation.ts +244 -0
- package/src/lib/types/annotation/audio.ts +33 -0
- package/src/lib/types/annotation/blockquote.ts +18 -0
- package/src/lib/types/annotation/code.ts +16 -0
- package/src/lib/types/annotation/deprecated.ts +18 -0
- package/src/lib/types/annotation/end-note-link.ts +35 -0
- package/src/lib/types/annotation/export.ts +69 -0
- package/src/lib/types/annotation/glossary-instance.ts +34 -0
- package/src/lib/types/annotation/has-abbreviation.ts +30 -0
- package/src/lib/types/annotation/heading.ts +37 -0
- package/src/lib/types/annotation/image.ts +29 -0
- package/src/lib/types/annotation/indent.ts +16 -0
- package/src/lib/types/annotation/index.ts +4 -0
- package/src/lib/types/annotation/inline-title.ts +34 -0
- package/src/lib/types/annotation/internal-link.ts +86 -0
- package/src/lib/types/annotation/leading-space.ts +18 -0
- package/src/lib/types/annotation/line-group.ts +18 -0
- package/src/lib/types/annotation/line.ts +16 -0
- package/src/lib/types/annotation/link.ts +38 -0
- package/src/lib/types/annotation/list-item.ts +16 -0
- package/src/lib/types/annotation/list.ts +43 -0
- package/src/lib/types/annotation/mantra.ts +31 -0
- package/src/lib/types/annotation/mention.ts +64 -0
- package/src/lib/types/annotation/paragraph.ts +18 -0
- package/src/lib/types/annotation/quote.ts +30 -0
- package/src/lib/types/annotation/quoted.ts +30 -0
- package/src/lib/types/annotation/reference.ts +18 -0
- package/src/lib/types/annotation/span.ts +39 -0
- package/src/lib/types/annotation/table-body-data.ts +18 -0
- package/src/lib/types/annotation/table-body-header.ts +18 -0
- package/src/lib/types/annotation/table-body-row.ts +18 -0
- package/src/lib/types/annotation/table.ts +16 -0
- package/src/lib/types/annotation/trailer.ts +16 -0
- package/src/lib/types/annotation/transform.ts +140 -0
- package/src/lib/types/annotation/unknown.ts +16 -0
- package/src/lib/types/bibliography.ts +86 -0
- package/src/lib/types/canon.ts +148 -0
- package/src/lib/types/client.ts +3 -0
- package/src/lib/types/contributor.ts +84 -0
- package/src/lib/types/editor-content.ts +21 -0
- package/src/lib/types/folio.ts +28 -0
- package/src/lib/types/glossary-page.ts +204 -0
- package/src/lib/types/glossary.ts +114 -0
- package/src/lib/types/imprint.ts +159 -0
- package/src/lib/types/index.ts +22 -0
- package/src/lib/types/language.ts +64 -0
- package/src/lib/types/layout.ts +37 -0
- package/src/lib/types/library.ts +29 -0
- package/src/lib/types/passage.ts +220 -0
- package/src/lib/types/project.ts +200 -0
- package/src/lib/types/semver.ts +1 -0
- package/src/lib/types/title.ts +47 -0
- package/src/lib/types/toh.ts +1 -0
- package/src/lib/types/translation.ts +15 -0
- package/src/lib/types/user.ts +15 -0
- package/src/lib/types/work.ts +38 -0
- package/src/lib/use-bookmark.ts +45 -0
- package/src/ssr.ts +5 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +26 -0
- package/tsconfig.spec.json +22 -0
package/.babelrc
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
|
3
|
+
"ignorePatterns": ["!**/*"],
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
7
|
+
"rules": {}
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"files": ["*.ts", "*.tsx"],
|
|
11
|
+
"rules": {}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"files": ["*.js", "*.jsx"],
|
|
15
|
+
"rules": {}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/README.md
ADDED
package/jest.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
displayName: 'data-access',
|
|
3
|
+
preset: '../../jest.preset.js',
|
|
4
|
+
transform: {
|
|
5
|
+
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
|
6
|
+
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
|
7
|
+
},
|
|
8
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
9
|
+
coverageDirectory: '../../coverage/libs/data-access',
|
|
10
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eightyfourthousand/data-access",
|
|
3
|
+
"version": "2026.3.0",
|
|
4
|
+
"description": "Shared data access clients, DTO mappers, and domain types for 84000 applications.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/84000/84000-apps.git",
|
|
9
|
+
"directory": "libs/data-access"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/84000/84000-apps",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/84000/84000-apps/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@eightyfourthousand/lib-utils": "^2026.3.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"next": "^16.0.0",
|
|
24
|
+
"react": "^19.0.0",
|
|
25
|
+
"react-dom": "^19.0.0"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"keywords": [],
|
|
29
|
+
"author": ""
|
|
30
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "data-access",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/data-access/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/js:tsc",
|
|
10
|
+
"outputs": ["{options.outputPath}"],
|
|
11
|
+
"options": {
|
|
12
|
+
"outputPath": "dist/libs/data-access",
|
|
13
|
+
"main": "libs/data-access/src/index.ts",
|
|
14
|
+
"tsConfig": "libs/data-access/tsconfig.lib.json",
|
|
15
|
+
"rootDir": "libs/data-access/src",
|
|
16
|
+
"generateExportsField": true,
|
|
17
|
+
"additionalEntryPoints": ["libs/data-access/src/ssr.ts"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './lib/auth';
|
|
2
|
+
export * from './lib/bibliography';
|
|
3
|
+
export * from './lib/canon';
|
|
4
|
+
export * from './lib/client-browser';
|
|
5
|
+
export * from './lib/client-server';
|
|
6
|
+
export * from './lib/client-token';
|
|
7
|
+
export * from './lib/folio';
|
|
8
|
+
export * from './lib/glossary';
|
|
9
|
+
export * from './lib/imprint';
|
|
10
|
+
export * from './lib/panel-url';
|
|
11
|
+
export * from './lib/library';
|
|
12
|
+
export * from './lib/passage';
|
|
13
|
+
export * from './lib/projects';
|
|
14
|
+
export * from './lib/publications';
|
|
15
|
+
export * from './lib/storage';
|
|
16
|
+
export * from './lib/types';
|
|
17
|
+
export * from './lib/local-storage';
|
|
18
|
+
export * from './lib/use-bookmark';
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { jwtDecode } from 'jwt-decode';
|
|
2
|
+
import { DataClient, UserClaims, UserPermission, UserRole } from './types';
|
|
3
|
+
|
|
4
|
+
export const getSession = async ({ client }: { client: DataClient }) => {
|
|
5
|
+
const { data, error } = await client.auth.getSession();
|
|
6
|
+
if (error || !data?.session) {
|
|
7
|
+
console.info(`Failed to get session data: ${error}`);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { access_token } = data.session;
|
|
12
|
+
if (!access_token) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const decoded = jwtDecode(access_token);
|
|
18
|
+
const { user_role: role = 'reader' } = decoded as {
|
|
19
|
+
user_role: UserRole;
|
|
20
|
+
};
|
|
21
|
+
const claims: UserClaims = {
|
|
22
|
+
role,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return { claims, ...data.session };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Failed to decode JWT:', error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getUser = async ({ client }: { client: DataClient }) => {
|
|
33
|
+
const session = await getSession({ client });
|
|
34
|
+
if (!session) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { claims, user } = session;
|
|
39
|
+
const { role } = claims;
|
|
40
|
+
|
|
41
|
+
const { id, email, user_metadata: metadata } = user;
|
|
42
|
+
const { name, picture } = metadata;
|
|
43
|
+
|
|
44
|
+
if (!id || !email) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { error, data } = await client
|
|
49
|
+
.from('user_profiles')
|
|
50
|
+
.select('username, fullName:full_name, avatar:avatar_url, subscriptions')
|
|
51
|
+
.eq('id', id)
|
|
52
|
+
.single();
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
console.warn(`Failed to fetch user profile: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { username, fullName, avatar, subscriptions } = data || {};
|
|
59
|
+
const subsArray = Array.isArray(subscriptions) ? subscriptions : [];
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
email,
|
|
64
|
+
username: username || email,
|
|
65
|
+
name: fullName || name,
|
|
66
|
+
avatar: avatar || picture,
|
|
67
|
+
role,
|
|
68
|
+
subscriptions: subsArray,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const hasPermission = async ({
|
|
73
|
+
client,
|
|
74
|
+
permission,
|
|
75
|
+
}: {
|
|
76
|
+
client: DataClient;
|
|
77
|
+
permission: UserPermission;
|
|
78
|
+
}) => {
|
|
79
|
+
const { data, error } = await client.rpc('authorize', {
|
|
80
|
+
requested_permission: permission,
|
|
81
|
+
});
|
|
82
|
+
if (error) {
|
|
83
|
+
console.error(`Failed to check permission: ${error.message}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return data as boolean;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const loginWithGoogle = async ({
|
|
90
|
+
client,
|
|
91
|
+
redirectTo = undefined,
|
|
92
|
+
}: {
|
|
93
|
+
client: DataClient;
|
|
94
|
+
redirectTo: string | undefined;
|
|
95
|
+
}) => {
|
|
96
|
+
await client.auth.signInWithOAuth({
|
|
97
|
+
provider: 'google',
|
|
98
|
+
options: {
|
|
99
|
+
redirectTo,
|
|
100
|
+
queryParams: {
|
|
101
|
+
access_type: 'offline',
|
|
102
|
+
prompt: 'consent',
|
|
103
|
+
hd: '84000.co',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const loginWithApple = async ({
|
|
110
|
+
client,
|
|
111
|
+
redirectTo = undefined,
|
|
112
|
+
}: {
|
|
113
|
+
client: DataClient;
|
|
114
|
+
redirectTo: string | undefined;
|
|
115
|
+
}) => {
|
|
116
|
+
await client.auth.signInWithOAuth({
|
|
117
|
+
provider: 'apple',
|
|
118
|
+
options: {
|
|
119
|
+
redirectTo,
|
|
120
|
+
queryParams: {
|
|
121
|
+
scope: 'email name',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const loginWithEmail = async ({
|
|
128
|
+
client,
|
|
129
|
+
email,
|
|
130
|
+
password,
|
|
131
|
+
}: {
|
|
132
|
+
client: DataClient;
|
|
133
|
+
email: string;
|
|
134
|
+
password: string;
|
|
135
|
+
}) => {
|
|
136
|
+
const { error } = await client.auth.signInWithPassword({
|
|
137
|
+
email,
|
|
138
|
+
password,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (error) {
|
|
142
|
+
console.error(`Failed to login with email: ${error.message}`);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const logout = async ({ client }: { client: DataClient }) => {
|
|
148
|
+
await client.auth.signOut();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const resetPassword = async ({
|
|
152
|
+
client,
|
|
153
|
+
email,
|
|
154
|
+
redirectTo = undefined,
|
|
155
|
+
}: {
|
|
156
|
+
client: DataClient;
|
|
157
|
+
email: string;
|
|
158
|
+
redirectTo?: string;
|
|
159
|
+
}) => {
|
|
160
|
+
const { error } = await client.auth.resetPasswordForEmail(email, {
|
|
161
|
+
redirectTo,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (error) {
|
|
165
|
+
console.error(`Failed to reset password: ${error.message}`);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const signUpWithEmail = async ({
|
|
171
|
+
client,
|
|
172
|
+
email,
|
|
173
|
+
password,
|
|
174
|
+
redirectTo = undefined,
|
|
175
|
+
}: {
|
|
176
|
+
client: DataClient;
|
|
177
|
+
email: string;
|
|
178
|
+
password: string;
|
|
179
|
+
redirectTo?: string;
|
|
180
|
+
}) => {
|
|
181
|
+
const { error } = await client.auth.signUp({
|
|
182
|
+
email,
|
|
183
|
+
password,
|
|
184
|
+
options: {
|
|
185
|
+
emailRedirectTo: redirectTo,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (error) {
|
|
190
|
+
console.error(`Failed to sign up with email: ${error.message}`);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const deleteAccount = async ({ client }: { client: DataClient }) => {
|
|
196
|
+
const { error } = await client.rpc('scholar_account_delete');
|
|
197
|
+
if (error) {
|
|
198
|
+
console.error(`Failed to delete account: ${error.message}`);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await logout({ client });
|
|
203
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BibliographyEntriesDTO,
|
|
3
|
+
bibliographyEntriesFromDTO,
|
|
4
|
+
BibliographyEntryItemDTO,
|
|
5
|
+
bibliographyEntryItemFromDTO,
|
|
6
|
+
DataClient,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
export const getBibliographyEntries = async ({
|
|
10
|
+
client,
|
|
11
|
+
uuid,
|
|
12
|
+
}: {
|
|
13
|
+
client: DataClient;
|
|
14
|
+
uuid: string;
|
|
15
|
+
}) => {
|
|
16
|
+
const { data, error } = await client.rpc('show_bibliographies', {
|
|
17
|
+
v_work_uuid: uuid,
|
|
18
|
+
});
|
|
19
|
+
if (error) {
|
|
20
|
+
console.error(
|
|
21
|
+
`Error fetching glossary instances for work: ${uuid} `,
|
|
22
|
+
error,
|
|
23
|
+
);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dto = data as BibliographyEntriesDTO;
|
|
28
|
+
return bibliographyEntriesFromDTO(dto);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getBibliographyEntry = async ({
|
|
32
|
+
client,
|
|
33
|
+
uuid,
|
|
34
|
+
}: {
|
|
35
|
+
client: DataClient;
|
|
36
|
+
uuid: string;
|
|
37
|
+
}) => {
|
|
38
|
+
const { data, error } = await client
|
|
39
|
+
.from('bibliographies')
|
|
40
|
+
.select(
|
|
41
|
+
`
|
|
42
|
+
uuid::uuid,
|
|
43
|
+
work_uuid::uuid,
|
|
44
|
+
bibl_html::text
|
|
45
|
+
`,
|
|
46
|
+
)
|
|
47
|
+
.eq('uuid', uuid)
|
|
48
|
+
.single();
|
|
49
|
+
if (error) {
|
|
50
|
+
console.error(`Error fetching bibliography entry: ${uuid} `, error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const dto = data as BibliographyEntryItemDTO;
|
|
55
|
+
|
|
56
|
+
return bibliographyEntryItemFromDTO(dto);
|
|
57
|
+
};
|
package/src/lib/canon.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanonDTO,
|
|
3
|
+
CanonDetailDTO,
|
|
4
|
+
CanonNode,
|
|
5
|
+
CanonWorksDTO,
|
|
6
|
+
DataClient,
|
|
7
|
+
canonDetailFromDTO,
|
|
8
|
+
canonNodeFromDTO,
|
|
9
|
+
canonTreeFromDTOs,
|
|
10
|
+
caononWorksFromDTO,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
export const getCanonSections = async ({
|
|
14
|
+
client,
|
|
15
|
+
}: {
|
|
16
|
+
client: DataClient;
|
|
17
|
+
}): Promise<CanonNode[]> => {
|
|
18
|
+
const { data, error } = await client.rpc('scholar_canon_get_all');
|
|
19
|
+
|
|
20
|
+
if (error) {
|
|
21
|
+
console.error('Error fetching canon sections:', error);
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return data.map((item: CanonDTO) => canonNodeFromDTO(item));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getCanonTree = async ({ client }: { client: DataClient }) => {
|
|
29
|
+
const { data, error } = await client.rpc('scholar_canon_get_all');
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
console.error('Error fetching canon tree:', error);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return canonTreeFromDTOs(data as CanonDTO[]);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const getCanonSection = async ({
|
|
40
|
+
client,
|
|
41
|
+
uuid,
|
|
42
|
+
}: {
|
|
43
|
+
client: DataClient;
|
|
44
|
+
uuid: string;
|
|
45
|
+
}) => {
|
|
46
|
+
const { data, error } = await client.rpc('scholar_canon_get_detail', {
|
|
47
|
+
uuid_input: uuid,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
console.error('Error fetching canon section:', error);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return canonDetailFromDTO(data[0] as CanonDetailDTO);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const getCanonWorks = async ({
|
|
59
|
+
client,
|
|
60
|
+
uuids,
|
|
61
|
+
}: {
|
|
62
|
+
client: DataClient;
|
|
63
|
+
uuids: string[];
|
|
64
|
+
}) => {
|
|
65
|
+
const { data, error } = await client
|
|
66
|
+
.rpc('scholar_canon_get_works', {
|
|
67
|
+
uuids_input: uuids,
|
|
68
|
+
})
|
|
69
|
+
.single();
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
console.error('Error fetching canon works:', error);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return caononWorksFromDTO(data as CanonWorksDTO);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Travers the canon tree to find a node by its UUID */
|
|
80
|
+
export const findNodeByUuid = (
|
|
81
|
+
head: CanonNode[],
|
|
82
|
+
uuid: string,
|
|
83
|
+
): CanonNode | null => {
|
|
84
|
+
for (const node of head) {
|
|
85
|
+
if (node.uuid === uuid) {
|
|
86
|
+
return node;
|
|
87
|
+
}
|
|
88
|
+
if (node.children) {
|
|
89
|
+
const found = findNodeByUuid(node.children, uuid);
|
|
90
|
+
if (found) {
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Flatten the canon tree into a list of nodes */
|
|
99
|
+
export const flattenCanonTree = (head: CanonNode): CanonNode[] => {
|
|
100
|
+
const flatList: CanonNode[] = [head];
|
|
101
|
+
|
|
102
|
+
const traverse = (node: CanonNode) => {
|
|
103
|
+
flatList.push(node);
|
|
104
|
+
if (node.children) {
|
|
105
|
+
node.children.forEach(traverse);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
head.children?.forEach(traverse);
|
|
110
|
+
return flatList;
|
|
111
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createBrowserClient as createSupabaseClient } from '@supabase/ssr';
|
|
2
|
+
|
|
3
|
+
export const createBrowserClient = () => {
|
|
4
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
5
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
6
|
+
|
|
7
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
8
|
+
throw new Error('Missing Supabase credentials');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return createSupabaseClient(supabaseUrl, supabaseAnonKey);
|
|
12
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CookieMethodsServer,
|
|
3
|
+
createServerClient as createSupabaseClient,
|
|
4
|
+
} from '@supabase/ssr';
|
|
5
|
+
|
|
6
|
+
export const createServerClient = ({
|
|
7
|
+
cookies,
|
|
8
|
+
}: {
|
|
9
|
+
cookies: CookieMethodsServer;
|
|
10
|
+
}) => {
|
|
11
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
12
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
13
|
+
|
|
14
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
15
|
+
throw new Error('Missing Supabase credentials');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const client = createSupabaseClient(supabaseUrl, supabaseAnonKey, {
|
|
19
|
+
cookies,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// NOTE: we suppress the warning about getSession being called. But is is
|
|
23
|
+
// import not to rely on this method for authorization. Rather, it is
|
|
24
|
+
// convenient for early exists in server components.
|
|
25
|
+
//
|
|
26
|
+
// @ts-expect-error - suppressGetSessionWarning is protected
|
|
27
|
+
client.auth.suppressGetSessionWarning = true;
|
|
28
|
+
|
|
29
|
+
return client;
|
|
30
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createServerClient as createSupabaseClient } from './client-server';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
export const createServerClient = async () => {
|
|
5
|
+
const cookieStore = await cookies();
|
|
6
|
+
return createSupabaseClient({
|
|
7
|
+
cookies: {
|
|
8
|
+
getAll: () => {
|
|
9
|
+
return cookieStore.getAll();
|
|
10
|
+
},
|
|
11
|
+
setAll: (cookiesToSet) => {
|
|
12
|
+
try {
|
|
13
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
14
|
+
cookieStore.set(name, value, options),
|
|
15
|
+
);
|
|
16
|
+
} catch {
|
|
17
|
+
// The `setAll` method was called from a Server Component.
|
|
18
|
+
// This can be ignored if you have middleware refreshing
|
|
19
|
+
// user sessions.
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { DataClient } from './types/client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a Supabase client using an access token for authentication.
|
|
6
|
+
* Use this for API routes that receive tokens via Authorization header
|
|
7
|
+
* instead of cookies (e.g., cross-domain requests).
|
|
8
|
+
*/
|
|
9
|
+
export const createTokenClient = (accessToken: string): DataClient => {
|
|
10
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
11
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
12
|
+
|
|
13
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
14
|
+
throw new Error('Missing Supabase credentials');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return createClient(supabaseUrl, supabaseAnonKey, {
|
|
18
|
+
global: {
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${accessToken}`,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parseToh } from '@eightyfourthousand/lib-utils';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { Resend } from 'resend';
|
|
4
|
+
|
|
5
|
+
async function sendRevisionEmail({
|
|
6
|
+
toh,
|
|
7
|
+
type,
|
|
8
|
+
label,
|
|
9
|
+
body,
|
|
10
|
+
}: {
|
|
11
|
+
toh: string;
|
|
12
|
+
type: string;
|
|
13
|
+
label: string;
|
|
14
|
+
body: string;
|
|
15
|
+
}) {
|
|
16
|
+
const apiKey = process.env['RESEND_API_KEY'];
|
|
17
|
+
const from = process.env['FEEDBACK_SENDER'];
|
|
18
|
+
const to = process.env['FEEDBACK_RECIPIENT'];
|
|
19
|
+
|
|
20
|
+
if (!apiKey || !from || !to) {
|
|
21
|
+
throw new Error('Missing email configuration environment variables');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resend = new Resend(apiKey);
|
|
25
|
+
const trimmedLabel = label.split('\n').join(', ').trim();
|
|
26
|
+
|
|
27
|
+
const subject = `[Revision Suggestion] ${parseToh(toh)} - ${type} ${trimmedLabel}`;
|
|
28
|
+
|
|
29
|
+
const { error } = await resend.emails.send({
|
|
30
|
+
from,
|
|
31
|
+
to,
|
|
32
|
+
subject,
|
|
33
|
+
text: body,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (error) {
|
|
37
|
+
throw new Error(error.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handleFeedbackPost(request: NextRequest) {
|
|
42
|
+
try {
|
|
43
|
+
const { toh, type, label, body } = await request.json();
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
typeof toh !== 'string' ||
|
|
47
|
+
!toh.trim() ||
|
|
48
|
+
typeof type !== 'string' ||
|
|
49
|
+
!type.trim() ||
|
|
50
|
+
typeof label !== 'string' ||
|
|
51
|
+
!label.trim() ||
|
|
52
|
+
typeof body !== 'string' ||
|
|
53
|
+
!body.trim()
|
|
54
|
+
) {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: 'All fields (toh, type, label, body) are required.' },
|
|
57
|
+
{ status: 400 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await sendRevisionEmail({ toh, type, label, body });
|
|
62
|
+
|
|
63
|
+
return NextResponse.json({ success: true });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Failed to send revision email:', error);
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: 'Failed to send feedback.' },
|
|
68
|
+
{ status: 500 },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|