first commit

This commit is contained in:
2026-06-19 23:55:45 -07:00
commit f2e4730549
297 changed files with 30726 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import { BuildOptions, DeepPartial, Factory } from "fishery"
import { Model } from "@sequelize/core"
// Must keep type signature in sync with fishery's Factory type
// See api/node_modules/fishery/dist/factory.d.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class BaseFactory<T extends Model, I = any, C = T> extends Factory<T, I, C> {
// See https://thoughtbot.github.io/factory_bot/ref/build-strategies.html#attributes_for
attributesFor(params?: DeepPartial<T>, options?: BuildOptions<T, I>): T {
const model = this.build(params, options)
return model.dataValues
}
}
export default BaseFactory
+1
View File
@@ -0,0 +1 @@
export { nestedSaveAndAssociateIfNew } from "./nested-save-and-associate-if-new"
@@ -0,0 +1,162 @@
import { Model } from "@sequelize/core"
/**
* Recursively saves a Sequelize model instance along with its associated models if they are new (unsaved).
*
* It handles `BelongsTo` associations and ensures associated model is saved _before_ the current model,
* then propagates the foreign key relationship before saving the current model.
*
* It handles `HasMany` associations and ensures associated models are saved _after_ the current model,
* propagating the foreign key relationship before saving.
*
* It handles `HasOne` associations and ensures the associated model is saved _after_ the current model,
* propagating the foreign key relationship before saving.
*
* @template M - A Sequelize model type extending `Model`.
* @param {M} modelInstance - The Sequelize model instance to be saved along with its associated models.
* @returns {Promise<M>} - Returns the saved model instance.
*
* @example
* // Given a non-persisted TravelSegment model instance, being passed via a HasMany association to a TravelAuthorization model.
* // The TravelAuthorization is saved first, then the TravelSegment is updated with the travelAuthorizationId before it is saved.
*
* const travelSegment = travelSegmentFactory.build({
* departureOn: faker.date.past(),
* })
* const travelAuthorization = await travelAuthorizationFactory
* .associations({
* travelSegments: [travelSegment],
* })
* .transient({
* include: ["user", "travelSegments"],
* })
* .create({
* status: TravelAuthorization.Statuses.SUBMITTED,
* })
*
* @example
* // Given a persisted TravelAuthorization passed to a TravelSegment model instance via a BelongsTo association.
* // The TravelSegment is updated with the travelAuthorizationId before it is saved.
* const travelAuthorization = await travelAuthorizationFactory
* .transient({ roundTrip: true })
* .create()
* const whitehorse = await locationFactory.create({ city: "Whitehorse", province: "YT" })
* const vancouver = await locationFactory.create({ city: "Vancouver", province: "BC" })
* const travelSegment1 = await travelSegmentFactory
* .associations({
* travelAuthorization,
* departureLocation: whitehorse,
* arrivalLocation: vancouver,
* })
* .create({
* segmentNumber: 1,
* departureOn: new Date("2022-06-05"),
* departureTime: Stop.BEGINNING_OF_DAY,
* modeOfTransport: Stop.TravelMethods.AIRCRAFT,
* accommodationType: Stop.AccommodationTypes.HOTEL,
* })
*
* @example
* // Given a generic TravelAuthorization creation.
* // The factory will create the purpose and user associations if they are not provided.
* // Then the onCreated hook will call nestedSaveAndAssociateIfNew to save
* // the purpose and user associations as they are new.
* // Then those associations will propagate their foreign keys to the travelAuthorization before it is saved.
* const travelAuthorization = await travelAuthorizationFactory.create()
*/
export async function nestedSaveAndAssociateIfNew<M extends Model>(modelInstance: M): Promise<M> {
const modelClass = modelInstance.constructor as typeof Model
const associations = modelClass.associations
const belongsToAssociationNames: string[] = []
const hasManyAssociationNames: string[] = []
const hasOneAssociationNames: string[] = []
for (const [associationName, { associationType }] of Object.entries(associations)) {
if (associationType === "BelongsTo") {
belongsToAssociationNames.push(associationName)
} else if (associationType === "HasMany") {
hasManyAssociationNames.push(associationName)
} else if (associationType === "HasOne") {
hasOneAssociationNames.push(associationName)
} else {
// no-op - maybe I should warn?
}
}
for (const associationName of belongsToAssociationNames) {
const associationDefinition = associations[associationName]
const foreignKeyName = associationDefinition.foreignKey
let foreignKeyValue = modelInstance.get(foreignKeyName)
if (foreignKeyValue !== undefined) continue
// Maybe should be associationAccessor instead of as?
const associationAlias = associationDefinition.as as keyof M
const associatedInstance = modelInstance[associationAlias] as Model | undefined
if (associatedInstance === undefined) continue
// @ts-expect-error - TS doesn't know that targetKey is a property of associationDefinition
const { targetKey }: { targetKey: keyof typeof associatedInstance } = associationDefinition
if (associatedInstance.isNewRecord !== true) {
foreignKeyValue = associatedInstance.get(targetKey)
modelInstance.set(foreignKeyName, foreignKeyValue)
continue
}
const updatedAssocationInstance = await nestedSaveAndAssociateIfNew(associatedInstance)
foreignKeyValue = updatedAssocationInstance.get(targetKey)
modelInstance.set(foreignKeyName, foreignKeyValue)
}
if (modelInstance.isNewRecord === true) {
await modelInstance.save()
}
for (const associationName of hasManyAssociationNames) {
const associationDefinition = associations[associationName]
// Maybe should be associationAccessor instead of as?
const associationAlias = associationDefinition.as as keyof M
const associatedInstances = modelInstance[associationAlias] as Model[] | undefined
if (associatedInstances === undefined) continue
for (const associatedInstance of associatedInstances) {
if (associatedInstance.isNewRecord !== true) continue
// @ts-expect-error - TS doesn't know that sourceKey is a property of associationDefinition
const {
sourceKey,
foreignKey,
}: { sourceKey: keyof M; foreignKey: keyof typeof associatedInstance } = associationDefinition
const sourceKeyValue = modelInstance.get(sourceKey)
associatedInstance.set(foreignKey, sourceKeyValue)
await nestedSaveAndAssociateIfNew(associatedInstance)
}
}
for (const associationName of hasOneAssociationNames) {
const associationDefinition = associations[associationName]
// Maybe should be associationAccessor instead of as?
const associationAlias = associationDefinition.as as keyof M
const associatedInstance = modelInstance[associationAlias] as Model | undefined
if (associatedInstance === undefined) continue
if (associatedInstance.isNewRecord !== true) continue
// @ts-expect-error - TS doesn't know that sourceKey is a property of associationDefinition
const {
sourceKey,
foreignKey,
}: { sourceKey: keyof M; foreignKey: keyof typeof associatedInstance } = associationDefinition
const sourceKeyValue = modelInstance.get(sourceKey)
associatedInstance.set(foreignKey, sourceKeyValue)
await nestedSaveAndAssociateIfNew(associatedInstance)
}
return modelInstance
}
export default nestedSaveAndAssociateIfNew
+2
View File
@@ -0,0 +1,2 @@
// Factories
export { userFactory } from "./user-factory"
+32
View File
@@ -0,0 +1,32 @@
import { Factory } from "fishery"
import { faker } from "@faker-js/faker/locale/en_CA"
import { User } from "@/models"
export const userFactory = Factory.define<User>(({ sequence, params, onCreate }) => {
onCreate((user) => {
try {
return user.save()
} catch (error) {
console.error(error)
throw new Error(
`Could not create User with attributes: ${JSON.stringify(user.dataValues, null, 2)}`
)
}
})
const firstName = params.firstName || `${faker.person.firstName()}-${sequence}`
const lastName = params.lastName || faker.person.lastName()
const email = params.email || faker.internet.email({ firstName, lastName })
return User.build({
email,
auth0Subject: params.auth0Subject || email,
firstName,
lastName,
displayName: `${firstName} ${lastName}`,
roles: [User.Roles.USER],
})
})
export default userFactory