generated from alphane/template
Initial commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
AttributeNames,
|
||||
Attributes,
|
||||
CreationOptional,
|
||||
FindOptions,
|
||||
Model,
|
||||
ModelStatic,
|
||||
Op,
|
||||
WhereOptions,
|
||||
} from "@sequelize/core"
|
||||
|
||||
import { searchFieldsByTermsFactory } from "@/utils/search-fields-by-terms-factory"
|
||||
|
||||
// See api/node_modules/@sequelize/core/lib/model.d.ts -> Model
|
||||
export abstract class BaseModel<
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
|
||||
TModelAttributes extends {} = any,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
TCreationAttributes extends {} = TModelAttributes,
|
||||
> extends Model<TModelAttributes, TCreationAttributes> {
|
||||
declare id: CreationOptional<number>
|
||||
|
||||
static addSearchScope<M extends BaseModel>(this: ModelStatic<M>, fields: AttributeNames<M>[]) {
|
||||
const searchScopeFunction = searchFieldsByTermsFactory<M>(fields)
|
||||
this.addScope("search", searchScopeFunction)
|
||||
}
|
||||
|
||||
// static findByPk<M extends Model, R = Attributes<M>>(
|
||||
// this: ModelStatic<M>,
|
||||
// identifier: unknown,
|
||||
// options: FindByPkOptions<M> & { raw: true; rejectOnEmpty?: false },
|
||||
// ): Promise<R | null>;
|
||||
// static findByPk<M extends Model, R = Attributes<M>>(
|
||||
// this: ModelStatic<M>,
|
||||
// identifier: unknown,
|
||||
// options: NonNullFindByPkOptions<M> & { raw: true },
|
||||
// ): Promise<R>;
|
||||
// static findByPk<M extends Model>(
|
||||
// this: ModelStatic<M>,
|
||||
// identifier: unknown,
|
||||
// options: NonNullFindByPkOptions<M>,
|
||||
// ): Promise<M>;
|
||||
// static findByPk<M extends Model>(
|
||||
// this: ModelStatic<M>,
|
||||
// identifier: unknown,
|
||||
// options?: FindByPkOptions<M>,
|
||||
// ): Promise<M | null>;
|
||||
public static async findByIdentifierOrPk<M extends BaseModel>(
|
||||
this: ModelStatic<M>,
|
||||
identifierOrPk: string | number,
|
||||
options?: Omit<FindOptions<Attributes<M>>, "where">
|
||||
): Promise<M | null> {
|
||||
if (typeof identifierOrPk === "number" || !isNaN(Number(identifierOrPk))) {
|
||||
const primaryKey = identifierOrPk
|
||||
return this.findByPk(primaryKey, options)
|
||||
}
|
||||
|
||||
const identifier = identifierOrPk
|
||||
if (!("identifier" in this.getAttributes())) {
|
||||
throw new Error(`${this.name} does not have a 'identifier' attribute.`)
|
||||
}
|
||||
|
||||
return this.findOne({
|
||||
...options,
|
||||
// @ts-expect-error - We know that the model has a slug attribute, and are ignoring the TS error
|
||||
where: { identifier },
|
||||
})
|
||||
}
|
||||
|
||||
// See api/node_modules/@sequelize/core/lib/model.d.ts -> findAll
|
||||
// Taken from https://api.rubyonrails.org/v7.1.0/classes/ActiveRecord/Batches.html#method-i-find_each
|
||||
// Enforces sort by id, overwriting any supplied order
|
||||
public static async findEach<M extends BaseModel>(
|
||||
this: ModelStatic<M>,
|
||||
processFunction: (record: M) => Promise<void>
|
||||
): Promise<void>
|
||||
public static async findEach<M extends BaseModel, R = Attributes<M>>(
|
||||
this: ModelStatic<M>,
|
||||
options: Omit<FindOptions<Attributes<M>>, "raw"> & {
|
||||
raw: true
|
||||
batchSize?: number
|
||||
},
|
||||
processFunction: (record: R) => Promise<void>
|
||||
): Promise<void>
|
||||
public static async findEach<M extends BaseModel>(
|
||||
this: ModelStatic<M>,
|
||||
options: FindOptions<Attributes<M>> & {
|
||||
batchSize?: number
|
||||
},
|
||||
processFunction: (record: M) => Promise<void>
|
||||
): Promise<void>
|
||||
public static async findEach<M extends BaseModel, R = Attributes<M>>(
|
||||
this: ModelStatic<M>,
|
||||
optionsOrFunction:
|
||||
| ((record: M) => Promise<void>)
|
||||
| (Omit<FindOptions<Attributes<M>>, "raw"> & { raw: true; batchSize?: number })
|
||||
| (FindOptions<Attributes<M>> & { batchSize?: number }),
|
||||
maybeFunction?: (record: R | M) => Promise<void>
|
||||
): Promise<void> {
|
||||
let options:
|
||||
| (FindOptions<Attributes<M>> & { batchSize?: number })
|
||||
| (Omit<FindOptions<Attributes<M>>, "raw"> & { raw: true; batchSize?: number })
|
||||
|
||||
// TODO: fix types so that process function is M when not raw
|
||||
// and R when raw. Raw is usable, just incorrectly typed.
|
||||
let processFunction: (record: M) => Promise<void>
|
||||
|
||||
if (typeof optionsOrFunction === "function") {
|
||||
options = {}
|
||||
processFunction = optionsOrFunction
|
||||
} else if (maybeFunction === undefined) {
|
||||
throw new Error("findEach requires a processFunction")
|
||||
} else {
|
||||
options = optionsOrFunction
|
||||
processFunction = maybeFunction
|
||||
}
|
||||
|
||||
const batchSize = options.batchSize ?? 1000
|
||||
let lastId = 0
|
||||
let continueProcessing = true
|
||||
|
||||
while (continueProcessing) {
|
||||
// TODO: fix where option types so cast is not needed
|
||||
const whereClause = {
|
||||
...options.where,
|
||||
id: { [Op.gt]: lastId },
|
||||
} as WhereOptions<Attributes<M>>
|
||||
const records = await this.findAll({
|
||||
...options,
|
||||
where: whereClause,
|
||||
limit: batchSize,
|
||||
order: [["id", "ASC"]],
|
||||
})
|
||||
|
||||
for (const record of records) {
|
||||
await processFunction(record)
|
||||
lastId = record.id
|
||||
}
|
||||
|
||||
if (records.length < batchSize) {
|
||||
continueProcessing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseModel
|
||||
@@ -0,0 +1,19 @@
|
||||
import db from "@/db/db-client"
|
||||
|
||||
// Models
|
||||
import User, { UserRoles } from "@/models/user"
|
||||
|
||||
db.addModels([
|
||||
User,
|
||||
])
|
||||
|
||||
// Lazy load scopes
|
||||
User.establishScopes()
|
||||
|
||||
export {
|
||||
User,
|
||||
UserRoles,
|
||||
}
|
||||
|
||||
// Special db instance will all models loaded
|
||||
export default db
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
type CreationOptional,
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
type NonAttribute,
|
||||
sql,
|
||||
} from "@sequelize/core"
|
||||
import {
|
||||
Attribute,
|
||||
AutoIncrement,
|
||||
Default,
|
||||
Index,
|
||||
NotNull,
|
||||
PrimaryKey,
|
||||
ValidateAttribute,
|
||||
} from "@sequelize/core/decorators-legacy"
|
||||
import { isArray, isNil } from "lodash"
|
||||
|
||||
import BaseModel from "@/models/base-model"
|
||||
|
||||
/** Keep in sync with web/src/api/users-api.ts */
|
||||
export enum UserRoles {
|
||||
SYSTEM_ADMIN = "system_admin",
|
||||
USER = "user",
|
||||
}
|
||||
|
||||
export class User extends BaseModel<InferAttributes<User>, InferCreationAttributes<User>> {
|
||||
static readonly Roles = UserRoles
|
||||
|
||||
@Attribute(DataTypes.INTEGER)
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
declare id: CreationOptional<number>
|
||||
|
||||
@Attribute(DataTypes.STRING(100))
|
||||
@NotNull
|
||||
@Index({ unique: true })
|
||||
declare email: string
|
||||
|
||||
@Attribute(DataTypes.STRING(100))
|
||||
@NotNull
|
||||
@Index({ unique: true })
|
||||
declare auth0Subject: string
|
||||
|
||||
@Attribute(DataTypes.STRING(100))
|
||||
@NotNull
|
||||
declare firstName: string
|
||||
|
||||
@Attribute(DataTypes.STRING(100))
|
||||
@NotNull
|
||||
declare lastName: string
|
||||
|
||||
@Attribute(DataTypes.STRING(200))
|
||||
@NotNull
|
||||
declare displayName: string
|
||||
|
||||
@Attribute({
|
||||
type: DataTypes.STRING(255),
|
||||
get() {
|
||||
const roles = this.getDataValue("roles")
|
||||
if (isNil(roles)) {
|
||||
return []
|
||||
}
|
||||
return roles.split(",")
|
||||
},
|
||||
set(value: string[]) {
|
||||
this.setDataValue("roles", value.join(","))
|
||||
},
|
||||
})
|
||||
@NotNull
|
||||
@ValidateAttribute({
|
||||
validator: (valueString: string | string[]) => {
|
||||
const value = isArray(valueString) ? valueString : valueString.split(",")
|
||||
const validRoles = Object.values(UserRoles) as string[]
|
||||
const invalidRoles = value.filter((role) => !validRoles.includes(role))
|
||||
if (invalidRoles.length > 0) throw new Error(`Invalid role: ${invalidRoles.join(", ")}`)
|
||||
},
|
||||
})
|
||||
declare roles: UserRoles[]
|
||||
|
||||
@Attribute(DataTypes.DATE(0))
|
||||
@NotNull
|
||||
@Default(sql.literal("CURRENT_TIMESTAMP"))
|
||||
declare createdAt: CreationOptional<Date>
|
||||
|
||||
@Attribute(DataTypes.DATE(0))
|
||||
@NotNull
|
||||
@Default(sql.literal("CURRENT_TIMESTAMP"))
|
||||
declare updatedAt: CreationOptional<Date>
|
||||
|
||||
@Attribute(DataTypes.DATE(0))
|
||||
declare deletedAt: Date | null
|
||||
|
||||
// Magic Attributes
|
||||
get isSystemAdmin(): NonAttribute<boolean> {
|
||||
return this.roles.some((role) => role === UserRoles.SYSTEM_ADMIN)
|
||||
}
|
||||
|
||||
// Associations
|
||||
|
||||
// Scopes
|
||||
static establishScopes(): void {
|
||||
this.addSearchScope(["firstName", "lastName", "displayName", "email"])
|
||||
|
||||
this.addScope("asCurrentUser", {})
|
||||
}
|
||||
}
|
||||
|
||||
export default User
|
||||
Reference in New Issue
Block a user