WIP - additional fixes for multitenant

This commit is contained in:
Francisco Gaona
2025-11-30 10:09:21 +01:00
parent 57f27d28cd
commit 5a80f33078
12 changed files with 206 additions and 165 deletions

View File

@@ -2,8 +2,10 @@ import { Module } from '@nestjs/common';
import { AppBuilderService } from './app-builder.service';
import { RuntimeAppController } from './runtime-app.controller';
import { SetupAppController } from './setup-app.controller';
import { TenantModule } from '../tenant/tenant.module';
@Module({
imports: [TenantModule],
providers: [AppBuilderService],
controllers: [RuntimeAppController, SetupAppController],
exports: [AppBuilderService],

View File

@@ -1,44 +1,26 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { TenantDatabaseService } from '../tenant/tenant-database.service';
import { App } from '../models/app.model';
import { AppPage } from '../models/app-page.model';
import { ObjectDefinition } from '../models/object-definition.model';
@Injectable()
export class AppBuilderService {
constructor(private prisma: PrismaService) {}
constructor(private tenantDbService: TenantDatabaseService) {}
// Runtime endpoints
async getApps(tenantId: string, userId: string) {
// For now, return all active apps for the tenant
const knex = await this.tenantDbService.getTenantKnex(tenantId);
// For now, return all apps
// In production, you'd filter by user permissions
return this.prisma.app.findMany({
where: {
tenantId,
isActive: true,
},
include: {
pages: {
where: { isActive: true },
orderBy: { sortOrder: 'asc' },
},
},
orderBy: { label: 'asc' },
});
return App.query(knex).withGraphFetched('pages').orderBy('label', 'asc');
}
async getApp(tenantId: string, slug: string, userId: string) {
const app = await this.prisma.app.findUnique({
where: {
tenantId_slug: {
tenantId,
slug,
},
},
include: {
pages: {
where: { isActive: true },
orderBy: { sortOrder: 'asc' },
},
},
});
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const app = await App.query(knex)
.findOne({ slug })
.withGraphFetched('pages');
if (!app) {
throw new NotFoundException(`App ${slug} not found`);
@@ -53,23 +35,12 @@ export class AppBuilderService {
pageSlug: string,
userId: string,
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const app = await this.getApp(tenantId, appSlug, userId);
const page = await this.prisma.appPage.findFirst({
where: {
appId: app.id,
slug: pageSlug,
isActive: true,
},
include: {
object: {
include: {
fields: {
where: { isActive: true },
},
},
},
},
const page = await AppPage.query(knex).findOne({
appId: app.id,
slug: pageSlug,
});
if (!page) {
@@ -81,31 +52,15 @@ export class AppBuilderService {
// Setup endpoints
async getAllApps(tenantId: string) {
return this.prisma.app.findMany({
where: { tenantId },
include: {
pages: {
orderBy: { sortOrder: 'asc' },
},
},
orderBy: { label: 'asc' },
});
const knex = await this.tenantDbService.getTenantKnex(tenantId);
return App.query(knex).withGraphFetched('pages').orderBy('label', 'asc');
}
async getAppForSetup(tenantId: string, slug: string) {
const app = await this.prisma.app.findUnique({
where: {
tenantId_slug: {
tenantId,
slug,
},
},
include: {
pages: {
orderBy: { sortOrder: 'asc' },
},
},
});
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const app = await App.query(knex)
.findOne({ slug })
.withGraphFetched('pages');
if (!app) {
throw new NotFoundException(`App ${slug} not found`);
@@ -120,14 +75,12 @@ export class AppBuilderService {
slug: string;
label: string;
description?: string;
icon?: string;
},
) {
return this.prisma.app.create({
data: {
tenantId,
...data,
},
const knex = await this.tenantDbService.getTenantKnex(tenantId);
return App.query(knex).insert({
...data,
displayOrder: 0,
});
}
@@ -137,16 +90,12 @@ export class AppBuilderService {
data: {
label?: string;
description?: string;
icon?: string;
isActive?: boolean;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const app = await this.getAppForSetup(tenantId, slug);
return this.prisma.app.update({
where: { id: app.id },
data,
});
return App.query(knex).patchAndFetchById(app.id, data);
}
async createPage(
@@ -157,37 +106,19 @@ export class AppBuilderService {
label: string;
type: string;
objectApiName?: string;
config?: any;
sortOrder?: number;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const app = await this.getAppForSetup(tenantId, appSlug);
// If objectApiName is provided, find the object
let objectId: string | undefined;
if (data.objectApiName) {
const obj = await this.prisma.objectDefinition.findUnique({
where: {
tenantId_apiName: {
tenantId,
apiName: data.objectApiName,
},
},
});
objectId = obj?.id;
}
return this.prisma.appPage.create({
data: {
appId: app.id,
slug: data.slug,
label: data.label,
type: data.type,
objectApiName: data.objectApiName,
objectId,
config: data.config,
sortOrder: data.sortOrder || 0,
},
return AppPage.query(knex).insert({
appId: app.id,
slug: data.slug,
label: data.label,
type: data.type,
objectApiName: data.objectApiName,
displayOrder: data.sortOrder || 0,
});
}
@@ -199,44 +130,24 @@ export class AppBuilderService {
label?: string;
type?: string;
objectApiName?: string;
config?: any;
sortOrder?: number;
isActive?: boolean;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const app = await this.getAppForSetup(tenantId, appSlug);
const page = await this.prisma.appPage.findFirst({
where: {
appId: app.id,
slug: pageSlug,
},
const page = await AppPage.query(knex).findOne({
appId: app.id,
slug: pageSlug,
});
if (!page) {
throw new NotFoundException(`Page ${pageSlug} not found`);
}
// If objectApiName is provided, find the object
let objectId: string | undefined;
if (data.objectApiName) {
const obj = await this.prisma.objectDefinition.findUnique({
where: {
tenantId_apiName: {
tenantId,
apiName: data.objectApiName,
},
},
});
objectId = obj?.id;
}
return this.prisma.appPage.update({
where: { id: page.id },
data: {
...data,
objectId,
},
return AppPage.query(knex).patchAndFetchById(page.id, {
...data,
displayOrder: data.sortOrder,
});
}
}

View File

@@ -12,7 +12,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { TenantId } from '../tenant/tenant.decorator';
@Controller('setup/apps')
@UseGuards(JwtAuthGuard)
//@UseGuards(JwtAuthGuard)
export class SetupAppController {
constructor(private appBuilderService: AppBuilderService) {}
@@ -59,11 +59,6 @@ export class SetupAppController {
@Param('pageSlug') pageSlug: string,
@Body() data: any,
) {
return this.appBuilderService.updatePage(
tenantId,
appSlug,
pageSlug,
data,
);
return this.appBuilderService.updatePage(tenantId, appSlug, pageSlug, data);
}
}

View File

@@ -1,4 +1,5 @@
import { BaseModel } from './base.model';
import { App } from './app.model';
export class AppPage extends BaseModel {
static tableName = 'app_pages';
@@ -14,7 +15,7 @@ export class AppPage extends BaseModel {
static relationMappings = {
app: {
relation: BaseModel.BelongsToOneRelation,
modelClass: 'app.model',
modelClass: App,
join: {
from: 'app_pages.appId',
to: 'apps.id',

View File

@@ -1,4 +1,5 @@
import { BaseModel } from './base.model';
import { AppPage } from './app-page.model';
export class App extends BaseModel {
static tableName = 'apps';
@@ -12,7 +13,7 @@ export class App extends BaseModel {
static relationMappings = {
pages: {
relation: BaseModel.HasManyRelation,
modelClass: 'app-page.model',
modelClass: AppPage,
join: {
from: 'apps.id',
to: 'app_pages.appId',

View File

@@ -1,6 +1,8 @@
import { Model, ModelOptions, QueryContext } from 'objection';
import { Model, ModelOptions, QueryContext, snakeCaseMappers } from 'objection';
export class BaseModel extends Model {
static columnNameMappers = snakeCaseMappers();
id: string;
createdAt: Date;
updatedAt: Date;

View File

@@ -30,7 +30,7 @@ export class SchemaManagementService {
// Custom fields from field definitions
for (const field of fields) {
this.addFieldToTable(table, field);
this.addFieldColumn(table, field);
}
});
@@ -48,7 +48,7 @@ export class SchemaManagementService {
const tableName = this.getTableName(objectApiName);
await knex.schema.alterTable(tableName, (table) => {
this.addFieldToTable(table, field);
this.addFieldColumn(table, field);
});
this.logger.log(`Added field ${field.apiName} to table ${tableName}`);
@@ -85,7 +85,7 @@ export class SchemaManagementService {
/**
* Add a field column to a table builder
*/
private addFieldToTable(
private addFieldColumn(
table: Knex.CreateTableBuilder | Knex.AlterTableBuilder,
field: FieldDefinition,
) {

View File

@@ -180,8 +180,9 @@ export class TenantProvisioningService {
try {
// Create default roles
const [adminRoleId] = await tenantKnex('roles').insert({
id: tenantKnex.raw('(UUID())'),
const adminRoleId = crypto.randomUUID();
await tenantKnex('roles').insert({
id: adminRoleId,
name: 'Admin',
guardName: 'api',
description: 'Full system administrator access',
@@ -189,8 +190,9 @@ export class TenantProvisioningService {
updated_at: tenantKnex.fn.now(),
});
const [userRoleId] = await tenantKnex('roles').insert({
id: tenantKnex.raw('(UUID())'),
const userRoleId = crypto.randomUUID();
await tenantKnex('roles').insert({
id: userRoleId,
name: 'User',
guardName: 'api',
description: 'Standard user access',
@@ -212,7 +214,7 @@ export class TenantProvisioningService {
for (const perm of permissions) {
await tenantKnex('permissions').insert({
id: tenantKnex.raw('(UUID())'),
id: crypto.randomUUID(),
name: perm.name,
guardName: 'api',
description: perm.description,
@@ -225,7 +227,7 @@ export class TenantProvisioningService {
const allPermissions = await tenantKnex('permissions').select('id');
for (const perm of allPermissions) {
await tenantKnex('role_permissions').insert({
id: tenantKnex.raw('(UUID())'),
id: crypto.randomUUID(),
roleId: adminRoleId,
permissionId: perm.id,
created_at: tenantKnex.fn.now(),
@@ -239,7 +241,7 @@ export class TenantProvisioningService {
.select('id');
for (const perm of userPermissions) {
await tenantKnex('role_permissions').insert({
id: tenantKnex.raw('(UUID())'),
id: crypto.randomUUID(),
roleId: userRoleId,
permissionId: perm.id,
created_at: tenantKnex.fn.now(),