Add record access strategy

This commit is contained in:
Francisco Gaona
2026-01-05 07:48:22 +01:00
parent 838a010fb2
commit 16907aadf8
97 changed files with 11350 additions and 208 deletions

View File

@@ -0,0 +1,114 @@
import { Model, ModelOptions, QueryContext } from 'objection';
import { randomUUID } from 'crypto';
/**
* Central database models using Objection.js
* These models work with the central database (not tenant databases)
*/
export class CentralTenant extends Model {
static tableName = 'tenants';
id: string;
name: string;
slug: string;
dbHost: string;
dbPort: number;
dbName: string;
dbUsername: string;
dbPassword: string;
status: string;
createdAt: Date;
updatedAt: Date;
// Relations
domains?: CentralDomain[];
$beforeInsert(queryContext: QueryContext) {
this.id = this.id || randomUUID();
// Auto-generate slug from name if not provided
if (!this.slug && this.name) {
this.slug = this.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}
this.createdAt = new Date();
this.updatedAt = new Date();
}
$beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
this.updatedAt = new Date();
}
static get relationMappings() {
return {
domains: {
relation: Model.HasManyRelation,
modelClass: CentralDomain,
join: {
from: 'tenants.id',
to: 'domains.tenantId',
},
},
};
}
}
export class CentralDomain extends Model {
static tableName = 'domains';
id: string;
domain: string;
tenantId: string;
isPrimary: boolean;
createdAt: Date;
updatedAt: Date;
// Relations
tenant?: CentralTenant;
$beforeInsert(queryContext: QueryContext) {
this.id = this.id || randomUUID();
this.createdAt = new Date();
this.updatedAt = new Date();
}
$beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
this.updatedAt = new Date();
}
static get relationMappings() {
return {
tenant: {
relation: Model.BelongsToOneRelation,
modelClass: CentralTenant,
join: {
from: 'domains.tenantId',
to: 'tenants.id',
},
},
};
}
}
export class CentralUser extends Model {
static tableName = 'users';
id: string;
email: string;
password: string;
firstName: string | null;
lastName: string | null;
role: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
$beforeInsert(queryContext: QueryContext) {
this.id = this.id || randomUUID();
this.createdAt = new Date();
this.updatedAt = new Date();
}
$beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
this.updatedAt = new Date();
}
}

View File

@@ -74,5 +74,13 @@ export class FieldDefinition extends BaseModel {
to: 'object_definitions.id',
},
},
rolePermissions: {
relation: BaseModel.HasManyRelation,
modelClass: () => require('./role-field-permission.model').RoleFieldPermission,
join: {
from: 'field_definitions.id',
to: 'role_field_permissions.fieldDefinitionId',
},
},
};
}

View File

@@ -10,8 +10,11 @@ export class ObjectDefinition extends BaseModel {
description?: string;
isSystem: boolean;
isCustom: boolean;
orgWideDefault: 'private' | 'public_read' | 'public_read_write';
createdAt: Date;
updatedAt: Date;
fields?: any[];
rolePermissions?: any[];
static get jsonSchema() {
return {
@@ -25,12 +28,14 @@ export class ObjectDefinition extends BaseModel {
description: { type: 'string' },
isSystem: { type: 'boolean' },
isCustom: { type: 'boolean' },
orgWideDefault: { type: 'string', enum: ['private', 'public_read', 'public_read_write'] },
},
};
}
static get relationMappings() {
const { FieldDefinition } = require('./field-definition.model');
const { RoleObjectPermission } = require('./role-object-permission.model');
return {
fields: {
@@ -41,6 +46,14 @@ export class ObjectDefinition extends BaseModel {
to: 'field_definitions.objectDefinitionId',
},
},
rolePermissions: {
relation: BaseModel.HasManyRelation,
modelClass: RoleObjectPermission,
join: {
from: 'object_definitions.id',
to: 'role_object_permissions.objectDefinitionId',
},
},
};
}
}

View File

@@ -0,0 +1,113 @@
import { BaseModel } from './base.model';
export interface RecordShareAccessLevel {
canRead: boolean;
canEdit: boolean;
canDelete: boolean;
}
export class RecordShare extends BaseModel {
static tableName = 'record_shares';
// Don't use snake_case mapping since DB columns are already camelCase
static get columnNameMappers() {
return {
parse(obj: any) {
return obj;
},
format(obj: any) {
return obj;
},
};
}
// Don't auto-set timestamps - let DB defaults handle them
$beforeInsert() {
// Don't call super - skip BaseModel's timestamp logic
}
$beforeUpdate() {
// Don't call super - skip BaseModel's timestamp logic
}
id!: string;
objectDefinitionId!: string;
recordId!: string;
granteeUserId!: string;
grantedByUserId!: string;
accessLevel!: RecordShareAccessLevel;
expiresAt?: Date;
revokedAt?: Date;
createdAt!: Date;
updatedAt!: Date;
static get jsonSchema() {
return {
type: 'object',
required: ['objectDefinitionId', 'recordId', 'granteeUserId', 'grantedByUserId', 'accessLevel'],
properties: {
id: { type: 'string' },
objectDefinitionId: { type: 'string' },
recordId: { type: 'string' },
granteeUserId: { type: 'string' },
grantedByUserId: { type: 'string' },
accessLevel: {
type: 'object',
properties: {
canRead: { type: 'boolean' },
canEdit: { type: 'boolean' },
canDelete: { type: 'boolean' },
},
},
expiresAt: {
anyOf: [
{ type: 'string', format: 'date-time' },
{ type: 'null' },
{ type: 'object' } // Allow Date objects
]
},
revokedAt: {
anyOf: [
{ type: 'string', format: 'date-time' },
{ type: 'null' },
{ type: 'object' } // Allow Date objects
]
},
createdAt: { type: ['string', 'object'], format: 'date-time' },
updatedAt: { type: ['string', 'object'], format: 'date-time' },
},
};
}
static get relationMappings() {
const { ObjectDefinition } = require('./object-definition.model');
const { User } = require('./user.model');
return {
objectDefinition: {
relation: BaseModel.BelongsToOneRelation,
modelClass: ObjectDefinition,
join: {
from: 'record_shares.objectDefinitionId',
to: 'object_definitions.id',
},
},
granteeUser: {
relation: BaseModel.BelongsToOneRelation,
modelClass: User,
join: {
from: 'record_shares.granteeUserId',
to: 'users.id',
},
},
grantedByUser: {
relation: BaseModel.BelongsToOneRelation,
modelClass: User,
join: {
from: 'record_shares.grantedByUserId',
to: 'users.id',
},
},
};
}
}

View File

@@ -0,0 +1,51 @@
import { BaseModel } from './base.model';
export class RoleFieldPermission extends BaseModel {
static tableName = 'role_field_permissions';
id!: string;
roleId!: string;
fieldDefinitionId!: string;
canRead!: boolean;
canEdit!: boolean;
createdAt!: Date;
updatedAt!: Date;
static get jsonSchema() {
return {
type: 'object',
required: ['roleId', 'fieldDefinitionId'],
properties: {
id: { type: 'string' },
roleId: { type: 'string' },
fieldDefinitionId: { type: 'string' },
canRead: { type: 'boolean' },
canEdit: { type: 'boolean' },
},
};
}
static get relationMappings() {
const { Role } = require('./role.model');
const { FieldDefinition } = require('./field-definition.model');
return {
role: {
relation: BaseModel.BelongsToOneRelation,
modelClass: Role,
join: {
from: 'role_field_permissions.roleId',
to: 'roles.id',
},
},
fieldDefinition: {
relation: BaseModel.BelongsToOneRelation,
modelClass: FieldDefinition,
join: {
from: 'role_field_permissions.fieldDefinitionId',
to: 'field_definitions.id',
},
},
};
}
}

View File

@@ -0,0 +1,59 @@
import { BaseModel } from './base.model';
export class RoleObjectPermission extends BaseModel {
static tableName = 'role_object_permissions';
id!: string;
roleId!: string;
objectDefinitionId!: string;
canCreate!: boolean;
canRead!: boolean;
canEdit!: boolean;
canDelete!: boolean;
canViewAll!: boolean;
canModifyAll!: boolean;
createdAt!: Date;
updatedAt!: Date;
static get jsonSchema() {
return {
type: 'object',
required: ['roleId', 'objectDefinitionId'],
properties: {
id: { type: 'string' },
roleId: { type: 'string' },
objectDefinitionId: { type: 'string' },
canCreate: { type: 'boolean' },
canRead: { type: 'boolean' },
canEdit: { type: 'boolean' },
canDelete: { type: 'boolean' },
canViewAll: { type: 'boolean' },
canModifyAll: { type: 'boolean' },
},
};
}
static get relationMappings() {
const { Role } = require('./role.model');
const { ObjectDefinition } = require('./object-definition.model');
return {
role: {
relation: BaseModel.BelongsToOneRelation,
modelClass: Role,
join: {
from: 'role_object_permissions.roleId',
to: 'roles.id',
},
},
objectDefinition: {
relation: BaseModel.BelongsToOneRelation,
modelClass: ObjectDefinition,
join: {
from: 'role_object_permissions.objectDefinitionId',
to: 'object_definitions.id',
},
},
};
}
}

View File

@@ -27,6 +27,8 @@ export class Role extends BaseModel {
const { RolePermission } = require('./role-permission.model');
const { Permission } = require('./permission.model');
const { User } = require('./user.model');
const { RoleObjectPermission } = require('./role-object-permission.model');
const { RoleFieldPermission } = require('./role-field-permission.model');
return {
rolePermissions: {
@@ -61,6 +63,22 @@ export class Role extends BaseModel {
to: 'users.id',
},
},
objectPermissions: {
relation: BaseModel.HasManyRelation,
modelClass: RoleObjectPermission,
join: {
from: 'roles.id',
to: 'role_object_permissions.roleId',
},
},
fieldPermissions: {
relation: BaseModel.HasManyRelation,
modelClass: RoleFieldPermission,
join: {
from: 'roles.id',
to: 'role_field_permissions.roleId',
},
},
};
}
}