Skip to content

Commit 6d4256e

Browse files
committed
feat(user): reintroduce model google-spreadsheet-api
Since removing it caused a bug related to cache. We could fix the bug, but it is not worthy since the repo is in maintenance. Revert #1697
1 parent f28ed50 commit 6d4256e

File tree

6 files changed

+131
-77
lines changed

6 files changed

+131
-77
lines changed

__tests__/__utils__/services.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ import { v4 as uuidv4 } from 'uuid'
99

1010
import type { Identity, KratosDB } from '~/context/auth-services'
1111
import { Model } from '~/internals/graphql'
12-
13-
enum MajorDimension {
14-
Rows = 'ROWS',
15-
Columns = 'COLUMNS',
16-
}
12+
import type { MajorDimension } from '~/model'
1713

1814
export class MockKratos {
1915
identities: Identity[] = []

__tests__/schema/uuid/user.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import {
1717
} from '../../__utils__'
1818
import { Model } from '~/internals/graphql'
1919
import { Instance } from '~/types'
20-
21-
enum MajorDimension {
22-
Rows = 'ROWS',
23-
Columns = 'COLUMNS',
24-
}
20+
import { MajorDimension } from '~/model'
2521

2622
const client = new Client()
2723
const adminUserId = 1

packages/server/src/internals/data-source.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { RESTDataSource } from 'apollo-datasource-rest'
22

33
import { Context } from '~/context'
4-
import { createChatModel } from '~/model'
4+
import { createGoogleSpreadsheetApiModel, createChatModel } from '~/model'
55
import { createKratosModel } from '~/model/kratos'
66
import { createMailchimpModel } from '~/model/mailchimp'
77

88
export class ModelDataSource extends RESTDataSource {
99
public chat: ReturnType<typeof createChatModel>
10+
public googleSpreadsheetApi: ReturnType<
11+
typeof createGoogleSpreadsheetApiModel
12+
>
1013
public mailchimp: ReturnType<typeof createMailchimpModel>
1114
public kratos: ReturnType<typeof createKratosModel>
1215

@@ -19,6 +22,7 @@ export class ModelDataSource extends RESTDataSource {
1922
super()
2023

2124
this.chat = createChatModel({ context })
25+
this.googleSpreadsheetApi = createGoogleSpreadsheetApiModel({ context })
2226
this.mailchimp = createMailchimpModel()
2327
this.kratos = createKratosModel({ context })
2428
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { either as E, option as O, function as F } from 'fp-ts'
2+
import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
3+
import * as t from 'io-ts'
4+
import { nonEmptyArray } from 'io-ts-types/lib/nonEmptyArray'
5+
import { URL } from 'url'
6+
7+
import { Context } from '~/context'
8+
import { addContext, ErrorEvent } from '~/error-event'
9+
import { createLegacyQuery } from '~/internals/data-source-helper'
10+
11+
export enum MajorDimension {
12+
Rows = 'ROWS',
13+
Columns = 'COLUMNS',
14+
}
15+
16+
const CellValues = nonEmptyArray(t.array(t.string))
17+
// Syntax manually de-sugared because API Exporter doesn't support import() types yet
18+
// export type CellValues = t.TypeOf<typeof CellValues>
19+
export type CellValues = NonEmptyArray<string[]>
20+
21+
const ValueRange = t.intersection([
22+
t.partial({
23+
values: CellValues,
24+
}),
25+
t.type({
26+
range: t.string,
27+
majorDimension: t.string,
28+
}),
29+
])
30+
type ValueRange = t.TypeOf<typeof ValueRange>
31+
32+
interface Arguments {
33+
spreadsheetId: string
34+
range: string
35+
majorDimension?: MajorDimension
36+
}
37+
38+
export function createGoogleSpreadsheetApiModel({
39+
context,
40+
}: {
41+
context: Pick<Context, 'swrQueue' | 'cache'>
42+
}) {
43+
const getValues = createLegacyQuery<
44+
Arguments,
45+
E.Either<ErrorEvent, CellValues>
46+
>(
47+
{
48+
type: 'google-spreadsheets-api',
49+
enableSwr: true,
50+
getCurrentValue: async (args) => {
51+
const { spreadsheetId, range } = args
52+
const majorDimension = args.majorDimension ?? MajorDimension.Rows
53+
const url = new URL(
54+
`https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
55+
)
56+
url.searchParams.append('majorDimension', majorDimension)
57+
const apiSecret = process.env.GOOGLE_SPREADSHEET_API_SECRET
58+
url.searchParams.append('key', apiSecret)
59+
60+
const specifyErrorLocation = E.mapLeft(
61+
addContext({
62+
location: 'googleSpreadSheetApi',
63+
locationContext: { ...args },
64+
}),
65+
)
66+
67+
try {
68+
const response = await fetch(url.toString())
69+
70+
return F.pipe(
71+
ValueRange.decode(await response.json()),
72+
E.mapLeft(() => {
73+
return { error: new Error('invalid response') }
74+
}),
75+
E.map((v) => v.values),
76+
E.chain(E.fromNullable({ error: new Error('range is empty') })),
77+
specifyErrorLocation,
78+
)
79+
} catch (error) {
80+
return specifyErrorLocation(E.left({ error: E.toError(error) }))
81+
}
82+
},
83+
staleAfter: { hours: 1 },
84+
getKey: (args) => {
85+
const { spreadsheetId, range } = args
86+
const majorDimension = args.majorDimension ?? MajorDimension.Rows
87+
return `spreadsheet/${spreadsheetId}/${range}/${majorDimension}`
88+
},
89+
getPayload: (key) => {
90+
const parts = key.split('/')
91+
return parts.length === 4 && parts[0] === 'spreadsheet'
92+
? O.some({
93+
spreadsheetId: parts[1],
94+
range: parts[2],
95+
majorDimension: parts[3] as MajorDimension,
96+
})
97+
: O.none
98+
},
99+
examplePayload: {
100+
spreadsheetId: 'abc',
101+
range: 'Tabellenblatt1!A:F',
102+
majorDimension: MajorDimension.Rows,
103+
},
104+
},
105+
context,
106+
)
107+
108+
return {
109+
getValues,
110+
}
111+
}

packages/server/src/model/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { createChatModel } from './chat'
2+
import { createGoogleSpreadsheetApiModel } from './google-spreadsheet-api'
23
import { createKratosModel } from './kratos'
34
import { createMailchimpModel } from './mailchimp'
45

56
export * from './chat'
7+
export * from './google-spreadsheet-api'
68

79
export const modelFactories = {
810
chat: createChatModel,
11+
googleSpreadsheetApi: createGoogleSpreadsheetApiModel,
912
mailChimp: createMailchimpModel,
1013
kratos: createKratosModel,
1114
}

packages/server/src/schema/uuid/user/resolvers.ts

+10-66
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import * as serloAuth from '@serlo/authorization'
22
import { instanceToScope, Scope } from '@serlo/authorization'
33
import { createHash } from 'crypto'
44
import { array as A, either as E, function as F, option as O } from 'fp-ts'
5-
import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
65
import * as t from 'io-ts'
76
import * as R from 'ramda'
8-
import { URL } from 'url'
97

108
import { resolveUnrevisedEntityIds } from '../abstract-entity/resolvers'
119
import { UuidResolver } from '../abstract-uuid/resolvers'
@@ -27,6 +25,7 @@ import {
2725
generateRole,
2826
isGlobalRole,
2927
} from '~/internals/graphql'
28+
import { CellValues, MajorDimension } from '~/model'
3029
import { EntityDecoder, UserDecoder } from '~/model/decoder'
3130
import {
3231
getPermissionsForRole,
@@ -38,11 +37,6 @@ import { createThreadResolvers } from '~/schema/thread/utils'
3837
import { createUuidResolvers } from '~/schema/uuid/abstract-uuid/utils'
3938
import { Instance, Resolvers } from '~/types'
4039

41-
enum MajorDimension {
42-
Rows = 'ROWS',
43-
Columns = 'COLUMNS',
44-
}
45-
4640
export const ActiveUserIdsResolver = createCachedResolver<
4741
Record<string, never>,
4842
number[]
@@ -206,12 +200,12 @@ export const resolvers: Resolvers = {
206200
User: {
207201
...createUuidResolvers(),
208202
...createThreadResolvers(),
209-
async motivation(user, _args, _context) {
210-
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_API_MOTIVATION
211-
const range = 'Formularantworten!B:D'
212-
203+
async motivation(user, _args, context) {
213204
return F.pipe(
214-
await getSpreadsheetValues({ spreadsheetId, range }),
205+
await context.dataSources.model.googleSpreadsheetApi.getValues({
206+
spreadsheetId: process.env.GOOGLE_SPREADSHEET_API_MOTIVATION,
207+
range: 'Formularantworten!B:D',
208+
}),
215209
E.mapLeft(
216210
addContext({
217211
location: 'motivationSpreadsheet',
@@ -633,14 +627,11 @@ async function fetchActivityByType(
633627
return result
634628
}
635629

636-
async function activeDonorIDs(_context: Context) {
637-
const spreadsheetId = process.env.GOOGLE_SPREADSHEET_API_ACTIVE_DONORS
638-
const range = 'Tabellenblatt1!A:A'
639-
630+
async function activeDonorIDs(context: Context) {
640631
return F.pipe(
641-
await getSpreadsheetValues({
642-
spreadsheetId,
643-
range,
632+
await context.dataSources.model.googleSpreadsheetApi.getValues({
633+
spreadsheetId: process.env.GOOGLE_SPREADSHEET_API_ACTIVE_DONORS,
634+
range: 'Tabellenblatt1!A:A',
644635
majorDimension: MajorDimension.Columns,
645636
}),
646637
extractIDsFromFirstColumn,
@@ -689,50 +680,3 @@ async function deleteKratosUser(
689680
await authServices.kratos.admin.deleteIdentity({ id: identity.id })
690681
}
691682
}
692-
693-
type CellValues = NonEmptyArray<string[]>
694-
695-
interface GetSpreadsheetValuesArgs {
696-
spreadsheetId: string
697-
range: string
698-
majorDimension?: MajorDimension
699-
}
700-
701-
async function getSpreadsheetValues(
702-
args: GetSpreadsheetValuesArgs,
703-
): Promise<E.Either<ErrorEvent, CellValues>> {
704-
const { spreadsheetId, range } = args
705-
const majorDimension = args.majorDimension ?? MajorDimension.Rows
706-
const url = new URL(
707-
`https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
708-
)
709-
url.searchParams.append('majorDimension', majorDimension)
710-
const apiSecret = process.env.GOOGLE_SPREADSHEET_API_SECRET
711-
url.searchParams.append('key', apiSecret)
712-
713-
const specifyErrorLocation = E.mapLeft(
714-
addContext({
715-
location: 'googleSpreadSheetApi',
716-
locationContext: { ...args },
717-
}),
718-
)
719-
720-
try {
721-
const response = await fetch(url.toString())
722-
const data = (await response.json()) as { values?: string[][] }
723-
724-
if (
725-
!data.values ||
726-
!Array.isArray(data.values) ||
727-
data.values.length === 0
728-
) {
729-
return specifyErrorLocation(
730-
E.left({ error: new Error('invalid response or empty range') }),
731-
)
732-
}
733-
734-
return specifyErrorLocation(E.right(data.values as CellValues))
735-
} catch (error) {
736-
return specifyErrorLocation(E.left({ error: E.toError(error) }))
737-
}
738-
}

0 commit comments

Comments
 (0)