Skip to content

Commit

Permalink
feat: add a submissionService file and Submission type definitions
Browse files Browse the repository at this point in the history
Previously there were no service functions to allow users to get, add, or update submissions. There are now functions to perform these tasks. There is now also a Submission type alias in TypeDefs/dbschema.ts

Previously there were no service functions to allow users to get, add, or update submissions. There are now functions to perform these tasks. There is now also a Submission type alias in TypeDefs/dbschema.ts
  • Loading branch information
evan-desu committed Sep 8, 2023
1 parent 3bf64a5 commit 60ec5c3
Show file tree
Hide file tree
Showing 10 changed files with 510 additions and 309 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,26 @@ yarn test
9. If you'd like to share the query you built, such as demonstrating how you tested your code, check out [Apollo Explorer's sharing features](https://www.apollographql.com/blog/announcement/platform/save-and-share-your-graphql-operations-in-apollo-explorer/#sharing-a-collection).
</details>
# Troubleshooting
<details>
<summary>Firestore Indexing Error: "The query requires an index..."</summary>
When you running query using the `getSubmissions` method that requires ordering by a specific field, if an index hasn't been created for the combination of that field and the order direction, you might receive an error response. This response will typically contain a direct link to create the required index in the **Firebase Console**. Here's how you can proceed:
1. Click on the link in the `Error Response`: This link will redirect you to the Firebase Console, specifically to the Firestore section where you can create indices.
![image](docs/gql-studio-error-message.png)
2. In the Firebase Console, you should see a window labeled `Create or update indexes`. Click the `Save` button: This will initiate the process of creating the index. Index creation might take a few minutes.
![image](docs/firebase-create-index.jpeg)
3. Wait for the Index to be ready: Firestore will show the status of the index. Once it changes from `Building` to `Enabled`, you can proceed to run your GraphQL query again.
![image](docs/firebase-building-index.png)
4. Run Your GraphQL Query Again: With the index in place, your query should now execute without any errors related to indexing.
</details>
Binary file added docs/firebase-building-index.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/firebase-create-index.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/gql-studio-error-message.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const resolvers = {
},
submission: async (_parent: gqlType.Submission, args: { id: string }) => {
const matchingSubmission = await submissionService.getSubmissionById(args.id)

return matchingSubmission
}
},
Expand Down Expand Up @@ -89,7 +89,9 @@ const resolvers = {
})),
isUnderReview: true,
isApproved: false,
isRejected: false
isRejected: false,
createdDate: new Date().toISOString(),
updatedDate: new Date().toISOString()
}

const newSubmission = await submissionService.addSubmission(submissionData)
Expand Down
117 changes: 83 additions & 34 deletions src/services/submissionService.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,92 @@
import { DocumentData, getFirestore, Query } from 'firebase-admin/firestore'
import { DocumentData, Query } from 'firebase-admin/firestore'
import * as gqlType from '../typeDefs/gqlTypes'
import * as dbSchema from '../typeDefs/dbSchema'
import { dbInstance } from '../firebaseDb'

export const getSubmissionById = async (id: string) : Promise<dbSchema.Submission | null> => {
try {
const db = getFirestore()
const submissionRef = db.collection('submissions')
const submissionRef = dbInstance.collection('submissions')
const snapshot = await submissionRef.doc(id).get()

if (!snapshot.exists) {
return null
}

const submission = mapDbEntityTogqlEntity(snapshot.data() as DocumentData)

const convertedEntity = mapDbEntityTogqlEntity(snapshot.data() as DocumentData)

return convertedEntity
return submission
} catch (error) {
throw new Error(`Error retrieving submission: ${error}`)
}
}

export async function getSubmissions(filters: dbSchema.SubmissionSearchFilters = {}) {
try {
const db = getFirestore()
let subRef: Query<DocumentData> = db.collection('submissions')
const {
googleMapsUrl,
healthcareProfessionalName,
isUnderReview,
isApproved,
isRejected,
orderBy = [{
fieldToOrder: 'createdDate',
orderDirection: 'desc'
}],
limit = 20,
createdDate,
updatedDate,
spokenLanguages
} = filters

let subRef: Query<DocumentData> = dbInstance.collection('submissions')

if (googleMapsUrl) {
subRef = subRef.where('googleMapsUrl', '==', googleMapsUrl)
}

if (healthcareProfessionalName) {
subRef = subRef.where('healthcareProfessionalName', '==', healthcareProfessionalName)
}

if (typeof isUnderReview !== 'undefined') {
subRef = subRef.where('isUnderReview', '==', isUnderReview)
}

if (typeof isApproved !== 'undefined') {
subRef = subRef.where('isApproved', '==', isApproved)
}

if (filters.googleMapsUrl) {
subRef = subRef.where('googleMapsUrl', '==', filters.googleMapsUrl)
if (typeof isRejected !== 'undefined') {
subRef = subRef.where('isRejected', '==', isRejected)
}

if (filters.healthcareProfessionalName) {
subRef = subRef.where('healthcareProfessionalName', '==', filters.healthcareProfessionalName)
if (orderBy && Array.isArray(orderBy)) {
orderBy.forEach(order => {
if (order) {
subRef = subRef.orderBy(order.fieldToOrder, order.orderDirection)
}
})
}

if (typeof filters.isUnderReview !== 'undefined') {
subRef = subRef.where('isUnderReview', '==', filters.isUnderReview)
if (limit) {
subRef = subRef.limit(limit)
}

if (typeof filters.isApproved !== 'undefined') {
subRef = subRef.where('isApproved', '==', filters.isApproved)
if (filters.createdDate) {
subRef = subRef.where('createdDate', '==', filters.createdDate)
}

if (typeof filters.isRejected !== 'undefined') {
subRef = subRef.where('isRejected', '==', filters.isRejected)
if (updatedDate) {
subRef = subRef.where('updatedDate', '==', updatedDate)
}

const snapshot = await subRef.get()

let submissions = snapshot.docs.map(doc =>
mapDbEntityTogqlEntity({ ...doc.data(), id: doc.id}))

if (filters.spokenLanguages && filters.spokenLanguages.length) {
const requiredLanguages = filters.spokenLanguages
if (spokenLanguages && spokenLanguages.length) {
const requiredLanguages = spokenLanguages
.map((lang: { iso639_3: string }) => lang.iso639_3)

submissions = submissions.filter(sub =>
Expand Down Expand Up @@ -79,14 +114,17 @@ export const addSubmission = async (submission: gqlType.Submission):
try {
const dbSubmission = convertToDbSubmission(submission)

const db = getFirestore()
const submissionRef = db.collection('submissions')
const submissionRef = dbInstance.collection('submissions')

const docRef = await submissionRef.add(dbSubmission)

const currentDate = new Date().toISOString()

const newSubmission: dbSchema.Submission = {
...dbSubmission,
id: docRef.id
id: docRef.id,
createdDate: currentDate,
updatedDate: currentDate
}

return newSubmission
Expand All @@ -98,16 +136,16 @@ export const addSubmission = async (submission: gqlType.Submission):
export const updateSubmission = async (id: string, updatedFields: Partial<dbSchema.Submission>):
Promise<string> => {
try {
const db = getFirestore()
const submissionRef = db.collection('submissions').doc(id)
const submissionRef = dbInstance.collection('submissions').doc(id)

const snapshot = await submissionRef.get()

const submissionToUpdate = mapDbEntityTogqlEntity(snapshot.data() as DocumentData)

const updatedSubmission: dbSchema.Submission = {
...submissionToUpdate,
...updatedFields
...updatedFields,
updatedDate: new Date().toISOString()
}

await submissionRef.update(updatedSubmission)
Expand Down Expand Up @@ -174,7 +212,7 @@ export const mapAndValidateSpokenLanguages = (spokenLanguages: gqlType.SpokenLan
return validatedLanguages.map(gqlSpokenLanguageToDbSpokenLanguage)
}

export const mapGqlSearchFiltersToDbSearchFilters = (filters: gqlType.SubmissionSearchFilters):
export const mapGqlSearchFiltersToDbSearchFilters = (filters: gqlType.SubmissionSearchFilters = {}):
dbSchema.SubmissionSearchFilters => {
const mappedLanguages = filters.spokenLanguages?.map(lang => lang ? {
iso639_3: lang.iso639_3 ?? '',
Expand All @@ -186,17 +224,26 @@ export const mapGqlSearchFiltersToDbSearchFilters = (filters: gqlType.Submission
const filteredLanguages = (mappedLanguages ?? [])
.filter(lang => lang !== undefined) as dbSchema.SpokenLanguage[]

const mappedOrderBy = filters.orderBy ? filters.orderBy
.filter((o): o is gqlType.OrderBy => Boolean(o && o.fieldToOrder && o.orderDirection))
.map(order => ({
fieldToOrder: order.fieldToOrder as string,
orderDirection: order.orderDirection as dbSchema.OrderDirection
})) : undefined

return {
...filters,
// fix: false might be a better default for these booleans than undefined.
// fix: false might be a better default for these booleans than undefined.
googleMapsUrl: filters.googleMapsUrl === null ? undefined : filters.googleMapsUrl,
healthcareProfessionalName: filters.healthcareProfessionalName == null ? undefined :
healthcareProfessionalName: filters.healthcareProfessionalName === null ? undefined :
filters.healthcareProfessionalName,
spokenLanguages: filteredLanguages,
isUnderReview: filters.isUnderReview === null ? undefined : filters.isUnderReview,
isApproved: filters.isApproved === null ? undefined : filters.isApproved,
isRejected: filters.isRejected === null ? undefined : filters.isRejected
isUnderReview: filters.isUnderReview ?? undefined,
isApproved: filters.isApproved ?? undefined,
isRejected: filters.isRejected ?? undefined,
orderBy: mappedOrderBy,
limit: filters.limit === null ? undefined : filters.limit,
createdDate: filters.createdDate ?? undefined,
updatedDate: filters.updatedDate ?? undefined
}
}

Expand All @@ -208,7 +255,9 @@ const mapDbEntityTogqlEntity = (dbEntity: DocumentData) : dbSchema.Submission =>
spokenLanguages: dbEntity.spokenLanguages,
isUnderReview: dbEntity.isUnderReview,
isApproved: dbEntity.isApproved,
isRejected: dbEntity.isRejected
isRejected: dbEntity.isRejected,
createdDate: dbEntity.createdDate,
updatedDate: dbEntity.updatedDate
}

return gqlEntity
Expand Down
22 changes: 19 additions & 3 deletions src/typeDefs/dbSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PhysicalAddress } from './gqlTypes'
import { PhysicalAddress, SpokenLanguageInput } from './gqlTypes'

export type Contact = {
email: string
Expand Down Expand Up @@ -61,7 +61,9 @@ export type Submission = {
spokenLanguages: SpokenLanguage[],
isUnderReview: boolean,
isApproved: boolean,
isRejected: boolean
isRejected: boolean,
createdDate: string,
updatedDate: string
}

export type SubmissionSearchFilters = {
Expand All @@ -70,7 +72,21 @@ export type SubmissionSearchFilters = {
spokenLanguages?: SpokenLanguage[],
isUnderReview?: boolean,
isApproved?: boolean,
isRejected?: boolean
isRejected?: boolean,
orderBy?: OrderBy[],
limit?: number
createdDate?: string,
updatedDate?: string
}

export type OrderBy = {
orderDirection: OrderDirection,
fieldToOrder: string
}

export enum OrderDirection {
Asc = 'asc',
Desc = 'desc'
}

export type Degree = {
Expand Down
23 changes: 23 additions & 0 deletions src/typeDefs/gqlTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ export type MutationUpdateSubmissionArgs = {
input?: InputMaybe<SubmissionInput>;
};

export type OrderBy = {
fieldToOrder?: InputMaybe<Scalars['String']['input']>;
orderDirection?: InputMaybe<OrderDirection>;
};

export enum OrderDirection {
Asc = 'asc',
Desc = 'desc'
}

export type PhysicalAddress = {
__typename?: 'PhysicalAddress';
addressLine1En?: Maybe<Scalars['String']['output']>;
Expand Down Expand Up @@ -256,13 +266,15 @@ export type SpokenLanguageInput = {

export type Submission = {
__typename?: 'Submission';
createdDate: Scalars['String']['output'];
googleMapsUrl: Scalars['String']['output'];
healthcareProfessionalName: Scalars['String']['output'];
id: Scalars['ID']['output'];
isApproved: Scalars['Boolean']['output'];
isRejected: Scalars['Boolean']['output'];
isUnderReview: Scalars['Boolean']['output'];
spokenLanguages: Array<Maybe<SpokenLanguage>>;
updatedDate: Scalars['String']['output'];
};

export type SubmissionInput = {
Expand All @@ -272,12 +284,16 @@ export type SubmissionInput = {
};

export type SubmissionSearchFilters = {
createdDate?: InputMaybe<Scalars['String']['input']>;
googleMapsUrl?: InputMaybe<Scalars['String']['input']>;
healthcareProfessionalName?: InputMaybe<Scalars['String']['input']>;
isApproved?: InputMaybe<Scalars['Boolean']['input']>;
isRejected?: InputMaybe<Scalars['Boolean']['input']>;
isUnderReview?: InputMaybe<Scalars['Boolean']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
orderBy?: InputMaybe<Array<InputMaybe<OrderBy>>>;
spokenLanguages?: InputMaybe<Array<InputMaybe<SpokenLanguageInput>>>;
updatedDate?: InputMaybe<Scalars['String']['input']>;
};


Expand Down Expand Up @@ -362,10 +378,13 @@ export type ResolversTypes = {
HealthcareProfessionalInput: HealthcareProfessionalInput;
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
Insurance: Insurance;
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
Locale: Locale;
LocaleName: ResolverTypeWrapper<LocaleName>;
LocaleNameInput: LocaleNameInput;
Mutation: ResolverTypeWrapper<{}>;
OrderBy: OrderBy;
OrderDirection: OrderDirection;
PhysicalAddress: ResolverTypeWrapper<PhysicalAddress>;
PhysicalAddressInput: PhysicalAddressInput;
Query: ResolverTypeWrapper<{}>;
Expand Down Expand Up @@ -393,9 +412,11 @@ export type ResolversParentTypes = {
HealthcareProfessional: HealthcareProfessional;
HealthcareProfessionalInput: HealthcareProfessionalInput;
ID: Scalars['ID']['output'];
Int: Scalars['Int']['output'];
LocaleName: LocaleName;
LocaleNameInput: LocaleNameInput;
Mutation: {};
OrderBy: OrderBy;
PhysicalAddress: PhysicalAddress;
PhysicalAddressInput: PhysicalAddressInput;
Query: {};
Expand Down Expand Up @@ -513,13 +534,15 @@ export type SpokenLanguageResolvers<ContextType = any, ParentType extends Resolv
};

export type SubmissionResolvers<ContextType = any, ParentType extends ResolversParentTypes['Submission'] = ResolversParentTypes['Submission']> = {
createdDate?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
googleMapsUrl?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
healthcareProfessionalName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isApproved?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
isRejected?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
isUnderReview?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
spokenLanguages?: Resolver<Array<Maybe<ResolversTypes['SpokenLanguage']>>, ParentType, ContextType>;
updatedDate?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

Expand Down
Loading

0 comments on commit 60ec5c3

Please sign in to comment.