Skip to content

Commit

Permalink
Improve callable and schema validator types (#128)
Browse files Browse the repository at this point in the history
* Separate encoded output type

* Use z.Schema helper

* Add InferEncoded helper

* Provide void output interface to functions that return nothing

* Do not rely on zod on output types if not necessary

* Update DatabaseConverter

* Replace z.ZodType with z.Schema everywhere else

* Fix types

* Bring back UserInformation

* Replace z.Schema with z.ZodTypeAny

* Use output everywhere
  • Loading branch information
arkadiuszbachorski authored Sep 10, 2024
1 parent cc87beb commit ddbb20a
Show file tree
Hide file tree
Showing 26 changed files with 113 additions and 94 deletions.
2 changes: 2 additions & 0 deletions functions/models/src/functions/checkInvitationCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const checkInvitationCodeInputSchema = z.object({
export type CheckInvitationCodeInput = z.input<
typeof checkInvitationCodeInputSchema
>

export type CheckInvitationCodeOutput = undefined
9 changes: 3 additions & 6 deletions functions/models/src/functions/createInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ export const createInvitationInputSchema = z.object({
})
export type CreateInvitationInput = z.input<typeof createInvitationInputSchema>

export const createInvitationOutputSchema = z.object({
id: z.string(),
})
export type CreateInvitationOutput = z.output<
typeof createInvitationOutputSchema
>
export interface CreateInvitationOutput {
id: string
}
2 changes: 2 additions & 0 deletions functions/models/src/functions/deleteInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const deleteInvitationInputSchema = z.object({
invitationCode: z.string(),
})
export type DeleteInvitationInput = z.input<typeof deleteInvitationInputSchema>

export type DeleteInvitationOutput = undefined
2 changes: 2 additions & 0 deletions functions/models/src/functions/deleteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const deleteUserInputSchema = z.object({
userId: z.string(),
})
export type DeleteUserInput = z.input<typeof deleteUserInputSchema>

export type DeleteUserOutput = undefined
2 changes: 2 additions & 0 deletions functions/models/src/functions/dismissMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export const dismissMessageInputSchema = z.object({
didPerformAction: optionalishDefault(z.boolean(), false),
})
export type DismissMessageInput = z.input<typeof dismissMessageInputSchema>

export type DismissMessageOutput = undefined
9 changes: 3 additions & 6 deletions functions/models/src/functions/exportHealthSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ export type ExportHealthSummaryInput = z.input<
typeof exportHealthSummaryInputSchema
>

export const exportHealthSummaryOutputSchema = z.object({
content: z.string(),
})
export type ExportHealthSummaryOutput = z.output<
typeof exportHealthSummaryOutputSchema
>
export interface ExportHealthSummaryOutput {
content: string
}
39 changes: 15 additions & 24 deletions functions/models/src/functions/getUsersInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
//

import { z } from 'zod'
import { optionalish, optionalishDefault } from '../helpers/optionalish.js'
import { userConverter } from '../types/user.js'
import { userAuthConverter } from '../types/userAuth.js'
import { optionalishDefault } from '../helpers/optionalish.js'
import { type InferEncoded } from '../helpers/schemaConverter'
import { type userConverter } from '../types/user.js'
import { type userAuthConverter } from '../types/userAuth.js'

export const getUsersInformationInputSchema = z.object({
includeUserData: optionalishDefault(z.boolean(), false),
Expand All @@ -19,26 +20,16 @@ export type GetUsersInformationInput = z.input<
typeof getUsersInformationInputSchema
>

export const userInformationSchema = z.object({
auth: z.lazy(() => userAuthConverter.value.schema),
user: optionalish(z.lazy(() => userConverter.value.schema)),
})
export type UserInformation = z.output<typeof userInformationSchema>
export interface UserInformation {
auth: InferEncoded<typeof userAuthConverter>
user: InferEncoded<typeof userConverter> | undefined
}

export const getUsersInformationOutputSchema = z.record(
z
.object({
data: userInformationSchema,
})
.or(
z.object({
error: z.object({
code: z.string(),
message: z.string(),
}),
}),
),
)
export type GetUsersInformationOutput = z.output<
typeof getUsersInformationOutputSchema
export type GetUsersInformationOutput = Record<
string,
| {
data: UserInformation
error?: undefined
}
| { data?: undefined; error: { code: string; message: string } }
>
2 changes: 2 additions & 0 deletions functions/models/src/functions/registerDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ import { userDeviceConverter } from '../types/userDevice.js'

export const registerDeviceInputSchema = userDeviceConverter.value.schema
export type RegisterDeviceInput = z.input<typeof registerDeviceInputSchema>

export type RegisterDeviceOutput = undefined
2 changes: 2 additions & 0 deletions functions/models/src/functions/unregisterDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const unregisterDeviceInputSchema = z.object({
platform: z.nativeEnum(UserDevicePlatform),
})
export type UnregisterDeviceInput = z.input<typeof unregisterDeviceInputSchema>

export type UnregisterDeviceOutput = undefined
2 changes: 2 additions & 0 deletions functions/models/src/functions/updateUserInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export const updateUserInformationInputSchema = z.object({
export type UpdateUserInformationInput = z.input<
typeof updateUserInformationInputSchema
>

export type UpdateUserInformationOutput = undefined
6 changes: 2 additions & 4 deletions functions/models/src/helpers/optionalish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@

import { z } from 'zod'

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export function optionalish<T extends z.ZodType<any, any, any>>(type: T) {
export function optionalish<T extends z.ZodTypeAny>(type: T) {
return type.or(z.null().transform(() => undefined)).optional()
}

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export function optionalishDefault<T extends z.ZodType<any, any, any>>(
export function optionalishDefault<T extends z.ZodTypeAny>(
type: T,
defaultValue: z.output<T>,
) {
Expand Down
16 changes: 12 additions & 4 deletions functions/models/src/helpers/schemaConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
//

import { type z } from 'zod'
import { type Lazy } from './lazy'

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export class SchemaConverter<Schema extends z.ZodType<any, any, any>> {
export class SchemaConverter<Schema extends z.ZodTypeAny, Encoded> {
// Properties

readonly schema: Schema
readonly encode: (value: z.output<Schema>) => z.input<Schema>
readonly encode: (value: z.output<Schema>) => Encoded

get value(): this {
return this
Expand All @@ -23,9 +23,17 @@ export class SchemaConverter<Schema extends z.ZodType<any, any, any>> {

constructor(input: {
schema: Schema
encode: (value: z.output<Schema>) => z.input<Schema>
encode: (value: z.output<Schema>) => Encoded
}) {
this.schema = input.schema
this.encode = input.encode
}
}

/* eslint-disable @typescript-eslint/no-explicit-any */
export type InferEncoded<Input> =
Input extends SchemaConverter<any, any> ? ReturnType<Input['encode']>
: Input extends Lazy<SchemaConverter<any, any>> ?
ReturnType<Input['value']['encode']>
: never
/* eslint-enable @typescript-eslint/no-explicit-any */
7 changes: 5 additions & 2 deletions functions/src/functions/checkInvitationCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
// Based on:
// https://github.com/StanfordBDHG/PediatricAppleWatchStudy/pull/54/files

import { checkInvitationCodeInputSchema } from '@stanfordbdhg/engagehf-models'
import {
checkInvitationCodeInputSchema,
type CheckInvitationCodeOutput,
} from '@stanfordbdhg/engagehf-models'
import { https, logger } from 'firebase-functions/v2'
import { validatedOnCall } from './helpers.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'

export const checkInvitationCode = validatedOnCall(
'checkInvitationCode',
checkInvitationCodeInputSchema,
async (request): Promise<void> => {
async (request): Promise<CheckInvitationCodeOutput> => {
const factory = getServiceFactory()
const userId = factory.credential(request.auth).userId

Expand Down
6 changes: 3 additions & 3 deletions functions/src/functions/createInvitation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { expectError } from '../tests/helpers.js'

describeWithEmulators('function: createInvitation', (env) => {
it('should create an invitation for a clinician', async () => {
const input: z.output<typeof createInvitationInputSchema> = {
const input: z.input<typeof createInvitationInputSchema> = {
auth: {
displayName: 'Test User',
email: '[email protected]',
Expand Down Expand Up @@ -48,7 +48,7 @@ describeWithEmulators('function: createInvitation', (env) => {
})

it('should create an invitation for a patient', async () => {
const input: z.output<typeof createInvitationInputSchema> = {
const input: z.input<typeof createInvitationInputSchema> = {
auth: {
displayName: 'Test User',
email: '[email protected]',
Expand Down Expand Up @@ -79,7 +79,7 @@ describeWithEmulators('function: createInvitation', (env) => {
})

it('should not create an invitation without authentication', () => {
const input: z.output<typeof createInvitationInputSchema> = {
const input: z.input<typeof createInvitationInputSchema> = {
auth: {
displayName: 'Test User',
},
Expand Down
7 changes: 5 additions & 2 deletions functions/src/functions/deleteInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
// SPDX-License-Identifier: MIT
//

import { deleteInvitationInputSchema } from '@stanfordbdhg/engagehf-models'
import {
deleteInvitationInputSchema,
type DeleteInvitationOutput,
} from '@stanfordbdhg/engagehf-models'
import { validatedOnCall } from './helpers.js'
import { UserRole } from '../services/credential/credential.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'

export const deleteInvitation = validatedOnCall(
'deleteInvitation',
deleteInvitationInputSchema,
async (request): Promise<void> => {
async (request): Promise<DeleteInvitationOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)
const userService = factory.user()
Expand Down
7 changes: 5 additions & 2 deletions functions/src/functions/deleteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
// SPDX-License-Identifier: MIT
//

import { deleteUserInputSchema } from '@stanfordbdhg/engagehf-models'
import {
deleteUserInputSchema,
type DeleteUserOutput,
} from '@stanfordbdhg/engagehf-models'
import { validatedOnCall } from './helpers.js'
import { UserRole } from '../services/credential/credential.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'

export const deleteUser = validatedOnCall(
'deleteUser',
deleteUserInputSchema,
async (request): Promise<void> => {
async (request): Promise<DeleteUserOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)
const userService = factory.user()
Expand Down
7 changes: 5 additions & 2 deletions functions/src/functions/dismissMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
// SPDX-License-Identifier: MIT
//

import { dismissMessageInputSchema } from '@stanfordbdhg/engagehf-models'
import {
dismissMessageInputSchema,
type DismissMessageOutput,
} from '@stanfordbdhg/engagehf-models'
import { validatedOnCall } from './helpers.js'
import { UserRole } from '../services/credential/credential.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'

export const dismissMessage = validatedOnCall(
'dismissMessage',
dismissMessageInputSchema,
async (request): Promise<void> => {
async (request): Promise<DismissMessageOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)
const userId = request.data.userId ?? credential.userId
Expand Down
3 changes: 2 additions & 1 deletion functions/src/functions/exportHealthSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {
exportHealthSummaryInputSchema,
type ExportHealthSummaryOutput,
QuantityUnit,
} from '@stanfordbdhg/engagehf-models'
import { https } from 'firebase-functions/v2'
Expand All @@ -19,7 +20,7 @@ import { getServiceFactory } from '../services/factory/getServiceFactory.js'
export const exportHealthSummary = validatedOnCall(
'exportHealthSummary',
exportHealthSummaryInputSchema,
async (request): Promise<{ content: string }> => {
async (request): Promise<ExportHealthSummaryOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)

Expand Down
26 changes: 11 additions & 15 deletions functions/src/functions/getUsersInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,24 @@

import {
getUsersInformationInputSchema,
type getUsersInformationOutputSchema,
type GetUsersInformationOutput,
userAuthConverter,
userConverter,
type userInformationSchema,
} from '@stanfordbdhg/engagehf-models'
import { https } from 'firebase-functions'
import { type z } from 'zod'
import { validatedOnCall } from './helpers.js'
import { UserRole } from '../services/credential/credential.js'
import { getServiceFactory } from '../services/factory/getServiceFactory.js'

export const getUsersInformation = validatedOnCall(
'getUsersInformation',
getUsersInformationInputSchema,
async (request): Promise<z.input<typeof getUsersInformationOutputSchema>> => {
async (request): Promise<GetUsersInformationOutput> => {
const factory = getServiceFactory()
const credential = factory.credential(request.auth)
const userService = factory.user()

const result: z.input<typeof getUsersInformationOutputSchema> = {}
const result: GetUsersInformationOutput = {}
for (const userId of request.data.userIds) {
try {
const userData = await userService.getUser(userId)
Expand All @@ -43,18 +42,15 @@ export const getUsersInformation = validatedOnCall(
)

const user = await userService.getAuth(userId)
const userInformation: z.input<typeof userInformationSchema> = {
auth: {
displayName: user.displayName,
email: user.email,
phoneNumber: user.phoneNumber,
photoURL: user.photoURL,
result[userId] = {
data: {
auth: userAuthConverter.value.encode(user),
user:
request.data.includeUserData && userData !== undefined ?
userConverter.value.encode(userData.content)
: undefined,
},
}
if (request.data.includeUserData && userData !== undefined) {
userInformation.user = userConverter.value.encode(userData.content)
}
result[userId] = { data: userInformation }
} catch (error) {
if (error instanceof https.HttpsError) {
result[userId] = {
Expand Down
Loading

0 comments on commit ddbb20a

Please sign in to comment.