Skip to content

Instantly share code, notes, and snippets.

@mirashif
Created October 25, 2025 15:38
Show Gist options
  • Select an option

  • Save mirashif/2424875687c3b8273aa71539e0aab941 to your computer and use it in GitHub Desktop.

Select an option

Save mirashif/2424875687c3b8273aa71539e0aab941 to your computer and use it in GitHub Desktop.
Monkey patched Adminjs to support our multi-tenant app
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