Created
October 25, 2025 15:38
-
-
Save mirashif/2424875687c3b8273aa71539e0aab941 to your computer and use it in GitHub Desktop.
Monkey patched Adminjs to support our multi-tenant app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Resource, convertFilter, convertParam, getModelByName } from '@adminjs/prisma' | |
| import type { Filter } from 'adminjs' | |
| import { BaseRecord, flat } from 'adminjs' | |
| import { getPrismaClient, getPrismaClientAdminjs } from '../../prisma/util.js' | |
| import type { int } from 'aws-sdk/clients/datapipeline.js' | |
| import { DMMF } from '@prisma/client/runtime/library.js'; | |
| /* | |
| Our schema is: | |
| Booking: | |
| id: int | |
| ... | |
| and | |
| User: | |
| id: UUID | |
| booking: Booking[] | |
| ... | |
| Adminjs expects unique ids for CRUD operations | |
| But since we are fetching all booking from all tenants (and users) | |
| where booking ids are not unique across tenants | |
| Adminjs crashes because of duplicate ids | |
| So we are using booking.createdAt as id for booking | |
| */ | |
| export class CustomResource extends Resource { | |
| private readonly isBooking: boolean | |
| private readonly modelName: string | |
| private model_; | |
| constructor(args: any) { | |
| super(args) | |
| this.model_ = args.model | |
| this.modelName = this.id() | |
| this.isBooking = this.modelName === 'Booking' || this.modelName === 'User' | |
| } | |
| async find(filter: Filter, params?: Record<string, any>): Promise<BaseRecord[]> { | |
| if (!this.isBooking) return super.find(filter, params) | |
| let booking = await this.fetchBooking(filter) | |
| if (filter){ | |
| booking = booking.filter((v: any) => { | |
| const firstPath = filter.filters.name?.path | |
| const firstValue = filter.filters.name?.value | |
| // Find out the path name key of the value | |
| // For example, if the value is "John" | |
| // The path name key is "name" | |
| // So we can get the value from v["name"] | |
| if (firstPath === undefined) return true | |
| if (firstValue === undefined) return true | |
| if (v[firstPath].includes(firstValue)) return true | |
| if (v.tanent.includes(firstValue)) return true | |
| return false | |
| }) | |
| } | |
| return booking.map((v: any) => this.prepareValues(v)) | |
| } | |
| async findOne(id: number | string): Promise<BaseRecord | null> { | |
| if (!this.isBooking) return super.findOne(id) | |
| const booking = await this.fetchBooking() | |
| const filtered = this.filterBooking(booking, id) | |
| if (!filtered) return null | |
| return this.prepareValues(filtered) | |
| } | |
| async findMany(ids: (number | string)[]): Promise<BaseRecord[]> { | |
| if (!this.isBooking) return super.findMany(ids) | |
| const booking = await this.fetchBooking() | |
| const filtered = this.filterBooking(booking, ids) | |
| return filtered.map((v: any) => this.prepareValues(v)) | |
| } | |
| public async update(pk: string | number, params: Record<string, any> = {}): Promise<Record<string, any>> { | |
| if (this.modelName !== 'Booking') return super.update(pk, params) | |
| const preparedParams = this.CustomPrepareParams(params); | |
| // Change the id to realId | |
| // Because we are using createdAt as id | |
| // So we need to change it back to id | |
| // Morever, we need to change the type of id to int | |
| preparedParams.id = parseInt(params.realId) | |
| const client = getPrismaClient(params.tanent) | |
| const result = await client.booking.update({ | |
| where: { | |
| id: preparedParams.id, | |
| }, | |
| data: preparedParams, | |
| }); | |
| return this.customPrepareReturnValues(result); | |
| } | |
| private async fetchBooking(filter?: Filter) { | |
| const camelCase = (str: string) => str.charAt(0).toLowerCase() + str.slice(1) | |
| const prisma = getPrismaClientAdminjs() as Record<string, any> | |
| const tenants = await prisma.tenant.findMany() | |
| const bookingData: any[] = [] | |
| let fieldString = "" | |
| // Get all fields from Booking model | |
| // The type of fields is Record<string, any> | |
| const allFields: Record<string, any>[] = getModelByName(this.modelName).fields | |
| for (const field of allFields) { | |
| // As union can not cast enum, need to cast enum manually | |
| if (field.kind === 'object') { | |
| continue | |
| } | |
| else if (field.kind === 'enum') { | |
| fieldString += `CAST("${field.name}" AS text) AS "${field.name}",` | |
| } else { | |
| fieldString += `"${field.name}",` | |
| } | |
| } | |
| // Remove last comma | |
| fieldString = fieldString.substring(0, fieldString.length - 1) | |
| let SQLQuery = "" | |
| // Using postgresql search_path to fetch all booking from all tenants | |
| for (const tenant of tenants) { | |
| SQLQuery += `SELECT quote_ident('${tenant.identifier}') as tanent, | |
| ${fieldString} FROM "${tenant.identifier}"."${this.modelName}"` | |
| // Add union to combine all query | |
| if (tenant !== tenants[tenants.length - 1]) SQLQuery += ` UNION ALL ` | |
| } | |
| SQLQuery += ` ORDER BY "createdAt" DESC` | |
| // Need to use rawSQL to fetch all booking from all tenants | |
| return await prisma.$queryRawUnsafe(SQLQuery) | |
| } | |
| private filterBooking(data: any[], filter: (number | string)[] | number | string): any[] { | |
| if (!filter) return data | |
| const isArray = Array.isArray(filter) | |
| if (!isArray) { | |
| const id = filter.toString() | |
| if (this.modelName === 'User') return data.find(u => u.id === id) | |
| if (this.modelName === 'Booking') return data.find(b => b.createdAt.getTime().toString() === id) | |
| } | |
| if (isArray) { | |
| const ids = filter.map(id => id.toString()) | |
| if (this.modelName === 'User') return data.filter(u => ids.includes(u.id)) | |
| if (this.modelName === 'Booking') return data.filter(b => ids.includes(b.createdAt.getTime().toString())) | |
| } | |
| return [] | |
| } | |
| private customPrepareReturnValues(params: Record<string, any>): Record<string, any> { | |
| const preparedValues: Record<string, any> = {} | |
| for (const property of this.properties()) { | |
| const param = flat.get(params, property.path()) | |
| const key = property.path() | |
| if (param !== undefined && property.type() !== 'reference') { | |
| preparedValues[key] = param | |
| continue | |
| } | |
| const foreignColumnName = property.foreignColumnName() | |
| if (!foreignColumnName) continue | |
| preparedValues[key] = params[foreignColumnName] | |
| } | |
| const createdat: Date = params.createdAt | |
| if (params.flightJson !== undefined) { | |
| // This means it is booking | |
| preparedValues.id = createdat.getTime() | |
| preparedValues.realId = params.id | |
| preparedValues.tanent = params.tanent | |
| } | |
| return preparedValues | |
| } | |
| private prepareValues(params: Record<string, any>): BaseRecord { | |
| const preparedValues = this.customPrepareReturnValues(params) | |
| return new BaseRecord(preparedValues, this) | |
| } | |
| private CustomPrepareParams(params: Record<string, any>): Record<string, any> { | |
| const preparedParams: Record<string, any> = {}; | |
| for (const property of this.properties()) { | |
| const param = flat.get(params, property.path()); | |
| const key = property.path(); | |
| // eslint-disable-next-line no-continue | |
| if (param === undefined) continue; | |
| const type = property.type(); | |
| const foreignColumnName = property.foreignColumnName(); | |
| if (type === 'reference' && foreignColumnName) { | |
| preparedParams[foreignColumnName] = convertParam(property, this.model_.fields, param); | |
| // eslint-disable-next-line no-continue | |
| continue; | |
| } | |
| if (property.isArray()) { | |
| preparedParams[key] = param ? param.map((p: any) => convertParam(property, this.model_.fields, p)) : param | |
| } else { | |
| preparedParams[key] = convertParam(property, this.model_.fields, param) | |
| } | |
| } | |
| return preparedParams; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment