Skip to content

Commit

Permalink
Merge branch 'stage' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
moz-rotimib committed Apr 24, 2024
2 parents f721623 + 0f3016d commit f222dd5
Show file tree
Hide file tree
Showing 60 changed files with 2,521 additions and 532 deletions.
24 changes: 24 additions & 0 deletions .github/ISSUE_TEMPLATE/documentation_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: Documentation Request
about: Request new, updates, or changes to documentation
title: '[DOCS]'
labels: 'Documentation'
assignees: ''
---

**Topic of request**
What **topic** does this documentation request concern?
<br>

**Kind of request**
Is this a request for **new documentation**? An **update**? Or a **change** to existing docs?
<br>

If this is a request to create **new** documentation, what do you think is important to include?
<br>

If this is a request to **update** existing documention, what needs to be updated?
<br>

If this is a request to **change** existing documentation, what needs to be changed?
<br>
6 changes: 5 additions & 1 deletion common/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ export type Variant = {
token: string;
};

export type UserVariant = Variant & {
is_preferred_option: boolean;
}

/*
an object storing all
accent/locale/variant data for a user
*/
export type UserLanguage = {
locale: string;
variant?: Variant;
variant?: UserVariant;
accents?: Accent[];
};
2 changes: 1 addition & 1 deletion common/sentences.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export type SentenceSubmission = {
sentence: string
source: string
localeId: number
localeName: string
domains: string[]
variant?: string
}

export enum SentenceSubmissionError {
Expand Down
54 changes: 37 additions & 17 deletions server/src/api/sentences/handler/add-sentence-handler.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,67 @@
import { Request, Response } from 'express'
import * as TE from 'fp-ts/TaskEither'
import * as T from 'fp-ts/Task'
import * as O from 'fp-ts/Option'
import * as I from 'fp-ts/Identity'
import { pipe } from 'fp-ts/function'
import { AddSentenceCommandHandler } from '../../../application/sentences/use-case/command-handler/add-sentence-command-handler'
import { AddSentenceCommand } from '../../../application/sentences/use-case/command-handler/command/add-sentence-command'
import {
SentencesRepositoryErrorKind,
SentenceRepositoryErrorKind,
SentenceValidationErrorKind,
} from '../../../application/types/error'
import { createPresentableError } from '../../../application/helper/error-helper'
import { StatusCodes } from 'http-status-codes'
import { validateSentence } from '../../../core/sentences'
import {
findDomainIdByNameInDb,
saveSentenceInDb,
} from '../../../application/sentences/repository/sentences-repository'
import { findVariantByTagInDb } from '../../../application/sentences/repository/variant-repository'
import { findLocaleByNameInDb } from '../../../application/sentences/repository/locale-repository'

export default async (req: Request, res: Response) => {
const { sentence, localeId, localeName, source, domains } = req.body
const { sentence, localeName, source, domains, variant } = req.body

const command: AddSentenceCommand = {
clientId: req.client_id,
sentence: sentence,
localeId: localeId,
localeName: localeName,
source: source,
domains: domains
domains: domains,
variant: O.fromNullable(variant),
}

return pipe(
AddSentenceCommandHandler(command),
const cmdHandler = pipe(
AddSentenceCommandHandler,
I.ap(validateSentence),
I.ap(findDomainIdByNameInDb),
I.ap(findVariantByTagInDb),
I.ap(findLocaleByNameInDb),
I.ap(saveSentenceInDb)
)

const result = await pipe(
command,
cmdHandler,
TE.mapLeft(createPresentableError),
TE.match(
err => {
switch (err.kind) {
case SentencesRepositoryErrorKind: {
return T.of(res.status(StatusCodes.INTERNAL_SERVER_ERROR).json(err))
case SentenceRepositoryErrorKind: {
res.status(StatusCodes.INTERNAL_SERVER_ERROR)
break
}
case SentenceValidationErrorKind: {
res.status(StatusCodes.BAD_REQUEST)
break
}
case SentenceValidationErrorKind:
return T.of(res.status(StatusCodes.BAD_REQUEST).json(err))
}

return err
},
() =>
T.of(
res.json({
message: 'Sentence added successfully',
})
)
() => ({ message: 'Sentence added successfully' })
)
)()

return res.json(result)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@ import { sentenceDomains } from 'common'

export const AddSentenceRequest: AllowedSchema = {
type: 'object',
required: ['sentence', 'source', 'localeId', 'localeName', 'domains'],
required: ['sentence', 'source', 'localeName', 'domains'],
properties: {
sentence: {
type: 'string',
},
source: {
type: 'string',
},
localeId: {
type: 'integer',
minimum: 1,
},
localeName: {
type: 'string',
},
Expand All @@ -27,6 +23,9 @@ export const AddSentenceRequest: AllowedSchema = {
},
uniqueItems: true,
},
variant: {
type: 'string',
},
},
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/application/helper/error-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export const createSentenceValidationError = (
}
}

export const createPendingSentencesRepositoryError = createError(
'SentencesRepository'
export const createSentenceRepositoryError = createError(
'SentenceRepository'
)
export const createDatabaseError = createError('DatabaseError')

Expand Down
53 changes: 53 additions & 0 deletions server/src/application/sentences/repository/locale-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { option as O, taskEither as TE } from 'fp-ts'
import { ApplicationError } from '../../types/error'
import { queryDb } from '../../../infrastructure/db/mysql'
import { pipe } from 'fp-ts/lib/function'
import { createDatabaseError } from '../../helper/error-helper'
import { Locale, TextDirection } from '../../../core/types/locale'

export type FindLocaleByName = (
localeName: string
) => TE.TaskEither<ApplicationError, O.Option<Locale>>

type LocaleRow = {
id: number
name: string
targetSentenceCount: number
isContributable: number
isTranslated: number
textDirection: TextDirection
}

export const findLocaleByNameInDb: FindLocaleByName = (localeName: string) =>
pipe(
[localeName],
queryDb(
` SELECT
id,
name,
target_sentence_count as targetSentenceCount,
is_contributable as isContributable,
is_translated as isTranslated,
text_direction as textDirection
FROM locales
WHERE name = ?
`
),
TE.mapLeft((err: Error) =>
createDatabaseError(
`Error retrieving locale by name "${localeName}"`,
err
)
),
TE.map(([[result]]: Array<Array<LocaleRow>>) => {
return pipe(
result,
O.fromNullable,
O.map(result => ({
...result,
isContributable: result.isContributable === 1,
isTranslated: result.isTranslated === 1,
}))
)
})
)
62 changes: 41 additions & 21 deletions server/src/application/sentences/repository/sentences-repository.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { taskEither as TE, taskOption as TO } from 'fp-ts'
import { option as O, taskEither as TE, taskOption as TO } from 'fp-ts'
import { pipe } from 'fp-ts/lib/function'
import { MysqlError } from 'mysql2Types'
import { UnvalidatedSentence } from '../../../core/sentences/types'
import { SentencesForReviewRow } from '../../../infrastructure/db/types'
import Mysql, { getMySQLInstance } from '../../../lib/model/db/mysql'
import { createSentenceId } from '../../../lib/utility'
import { createPendingSentencesRepositoryError } from '../../helper/error-helper'
import { createSentenceRepositoryError } from '../../helper/error-helper'
import { ApplicationError } from '../../types/error'
import { SentenceSubmission } from '../../types/sentence-submission'

Expand All @@ -16,12 +16,22 @@ const db = getMySQLInstance()
const DUPLICATE_KEY_ERR = 1062
const BATCH_SIZE = 1000

export type SaveSentence =
(sentenceSubmission: SentenceSubmission) => TE.TaskEither<ApplicationError, void>

export type FindDomainIdByName =
(domainName: string) => TE.TaskEither<ApplicationError, O.Option<number>>

const insertSentenceTransaction = async (
db: Mysql,
sentence: SentenceSubmission
) => {
const sentenceId = createSentenceId(sentence.sentence, sentence.locale_id)
const conn = await mysql2.createConnection(db.getMysqlOptions())
const variant_id = pipe(
sentence.variant_id,
O.getOrElse(() => null)
)

try {
await conn.beginTransaction()
Expand All @@ -35,10 +45,10 @@ const insertSentenceTransaction = async (

await conn.query(
`
INSERT INTO sentence_metadata(sentence_id, client_id)
VALUES (?, ?);
INSERT INTO sentence_metadata(sentence_id, client_id, variant_id)
VALUES (?, ?, ?);
`,
[sentenceId, sentence.client_id]
[sentenceId, sentence.client_id, variant_id]
)

for (const domainId of sentence.domain_ids ?? []) {
Expand Down Expand Up @@ -126,22 +136,22 @@ const insertBulkSentencesTransaction = async (
}
}

const insertSentence =
const saveSentence =
(db: Mysql) =>
(
sentenceSubmission: SentenceSubmission
): TE.TaskEither<ApplicationError, unknown> => {
): TE.TaskEither<ApplicationError, void> => {
return TE.tryCatch(
() => insertSentenceTransaction(db, sentenceSubmission),
(err: MysqlError) => {
if (err.errno && err.errno === DUPLICATE_KEY_ERR) {
return createPendingSentencesRepositoryError(
return createSentenceRepositoryError(
`Duplicate entry '${sentenceSubmission.sentence}'`,
err
)
}

return createPendingSentencesRepositoryError(
return createSentenceRepositoryError(
`Error inserting pending sentence '${sentenceSubmission.sentence}'`,
err
)
Expand Down Expand Up @@ -178,7 +188,7 @@ const insertSentenceVote =
[vote.sentenceId, vote.vote, vote.clientId]
),
(err: Error) =>
createPendingSentencesRepositoryError(
createSentenceRepositoryError(
`Error inserting vote for pending_sentence ${vote.sentenceId} with client_id ${vote.clientId}`,
err
)
Expand All @@ -193,6 +203,7 @@ const toUnvalidatedSentence = ([unvalidatedSentenceRows]: [
sentenceId: row.id,
source: row.source,
localeId: row.locale_id,
variantTag: O.fromNullable(row.variant_token),
}))

const findSentencesForReview =
Expand All @@ -210,10 +221,13 @@ const findSentencesForReview =
sentences.text,
sentences.source,
sentences.locale_id,
variants.variant_token,
SUM(sentence_votes.vote) as number_of_approving_votes,
COUNT(sentence_votes.vote) as number_of_votes
FROM sentences
LEFT JOIN sentence_votes ON (sentence_votes.sentence_id=sentences.id)
LEFT JOIN sentence_metadata ON (sentence_metadata.sentence_id=sentences.id)
LEFT JOIN variants ON (variants.id=sentence_metadata.variant_id)
WHERE
sentences.is_validated = FALSE
AND sentences.locale_id = ?
Expand All @@ -238,20 +252,26 @@ const findSentencesForReview =

const findDomainIdByName =
(db: Mysql) =>
(domainName: string): TO.TaskOption<number> =>
TO.tryCatch(async () => {
const [[result]] = await db.query(
`
(domainName: string): TE.TaskEither<ApplicationError, O.Option<number>> =>
TE.tryCatch(
async () => {
const [[row]] = await db.query(
`
SELECT id FROM domains WHERE domain = ?
`,
[domainName]
)

return result ? Promise.resolve(result.id) : Promise.reject()
})
[domainName]
)
return row ? O.some(row.id) : O.none
},
(err: Error) =>
createSentenceRepositoryError(
`Error retrieving domain id for domain "${domainName}"`,
err
)
)

export const insertSentenceIntoDb = insertSentence(db)
export const saveSentenceInDb: SaveSentence = saveSentence(db)
export const insertBulkSentencesIntoDb = insertBulkSentences(db)
export const insertSentenceVoteIntoDb = insertSentenceVote(db)
export const findSentencesForReviewInDb = findSentencesForReview(db)
export const findDomainIdByNameInDb = findDomainIdByName(db)
export const findDomainIdByNameInDb: FindDomainIdByName = findDomainIdByName(db)

0 comments on commit f222dd5

Please sign in to comment.