mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-28 15:54:56 +00:00
Merge pull request #230 from johndoe6345789/codex/refactor-acl-adapter-structure-and-imports
Refactor ACL adapter into strategies
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
export { ACLAdapter } from './acl-adapter/index'
|
||||
export type { User, ACLRule } from './acl/types'
|
||||
export { ACLAdapter } from './acl-adapter'
|
||||
export type { ACLAdapterOptions, ACLContext, ACLRule, User } from './acl-adapter/types'
|
||||
export { defaultACLRules } from './acl/default-rules'
|
||||
|
||||
86
dbal/development/src/adapters/acl-adapter/acl-adapter.ts
Normal file
86
dbal/development/src/adapters/acl-adapter/acl-adapter.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { AdapterCapabilities, DBALAdapter } from '../adapter'
|
||||
import type { ListOptions, ListResult } from '../../core/foundation/types'
|
||||
import { createContext } from './context'
|
||||
import { createReadStrategy } from './read-strategy'
|
||||
import { createWriteStrategy } from './write-strategy'
|
||||
import type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types'
|
||||
|
||||
export class ACLAdapter implements DBALAdapter {
|
||||
private readonly context: ACLContext
|
||||
private readonly readStrategy: ReturnType<typeof createReadStrategy>
|
||||
private readonly writeStrategy: ReturnType<typeof createWriteStrategy>
|
||||
|
||||
constructor(baseAdapter: DBALAdapter, user: User, options?: ACLAdapterOptions) {
|
||||
this.context = createContext(baseAdapter, user, options)
|
||||
this.readStrategy = createReadStrategy(this.context)
|
||||
this.writeStrategy = createWriteStrategy(this.context)
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
return this.writeStrategy.create(entity, data)
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
return this.readStrategy.read(entity, id)
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
return this.writeStrategy.update(entity, id, data)
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
return this.writeStrategy.delete(entity, id)
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
return this.readStrategy.list(entity, options)
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
return this.readStrategy.findFirst(entity, filter)
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
return this.readStrategy.findByField(entity, field, value)
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
filter: Record<string, unknown>,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
return this.writeStrategy.upsert(entity, filter, createData, updateData)
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
return this.writeStrategy.updateByField(entity, field, value, data)
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
return this.writeStrategy.deleteByField(entity, field, value)
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
return this.writeStrategy.createMany(entity, data)
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
return this.writeStrategy.updateMany(entity, filter, data)
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
return this.writeStrategy.deleteMany(entity, filter)
|
||||
}
|
||||
|
||||
async getCapabilities(): Promise<AdapterCapabilities> {
|
||||
return this.context.baseAdapter.getCapabilities()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.context.baseAdapter.close()
|
||||
}
|
||||
}
|
||||
|
||||
export type { ACLAdapterOptions, ACLContext, ACLRule, User }
|
||||
export { defaultACLRules } from '../acl/default-rules'
|
||||
@@ -1,20 +1,12 @@
|
||||
import type { DBALAdapter } from '../adapter'
|
||||
import type { User, ACLRule } from '../acl/types'
|
||||
import type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types'
|
||||
import { logAudit } from '../acl/audit-logger'
|
||||
import { defaultACLRules } from '../acl/default-rules'
|
||||
|
||||
export interface ACLContext {
|
||||
baseAdapter: DBALAdapter
|
||||
user: User
|
||||
rules: ACLRule[]
|
||||
auditLog: boolean
|
||||
logger: (entity: string, operation: string, success: boolean, message?: string) => void
|
||||
}
|
||||
|
||||
export const createContext = (
|
||||
baseAdapter: DBALAdapter,
|
||||
user: User,
|
||||
options?: { rules?: ACLRule[]; auditLog?: boolean },
|
||||
options?: ACLAdapterOptions,
|
||||
): ACLContext => {
|
||||
const auditLog = options?.auditLog ?? true
|
||||
const rules = options?.rules || defaultACLRules
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { checkPermission } from '../acl/check-permission'
|
||||
import { checkRowLevelAccess } from '../acl/check-row-level-access'
|
||||
import { resolvePermissionOperation } from '../acl/resolve-permission-operation'
|
||||
import type { ACLContext } from './context'
|
||||
import type { ACLContext } from './types'
|
||||
|
||||
export const enforcePermission = (context: ACLContext, entity: string, operation: string) => {
|
||||
checkPermission(entity, operation, context.user, context.rules, context.logger)
|
||||
|
||||
@@ -1,92 +1,3 @@
|
||||
import type { AdapterCapabilities, DBALAdapter } from '../adapter'
|
||||
import type { ListOptions, ListResult } from '../../core/foundation/types'
|
||||
import type { User, ACLRule } from '../acl/types'
|
||||
import type { ACLContext } from './context'
|
||||
import { createContext } from './context'
|
||||
import { createEntity, deleteEntity, listEntities, readEntity, updateEntity } from './crud'
|
||||
import {
|
||||
createMany,
|
||||
deleteByField,
|
||||
deleteMany,
|
||||
findByField,
|
||||
findFirst,
|
||||
updateByField,
|
||||
updateMany,
|
||||
upsert,
|
||||
} from './bulk'
|
||||
|
||||
export class ACLAdapter implements DBALAdapter {
|
||||
private readonly context: ACLContext
|
||||
|
||||
constructor(baseAdapter: DBALAdapter, user: User, options?: { rules?: ACLRule[]; auditLog?: boolean }) {
|
||||
this.context = createContext(baseAdapter, user, options)
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
return createEntity(this.context)(entity, data)
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
return readEntity(this.context)(entity, id)
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
return updateEntity(this.context)(entity, id, data)
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
return deleteEntity(this.context)(entity, id)
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
return listEntities(this.context)(entity, options)
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
return findFirst(this.context)(entity, filter)
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
return findByField(this.context)(entity, field, value)
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
filter: Record<string, unknown>,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
return upsert(this.context)(entity, filter, createData, updateData)
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
return updateByField(this.context)(entity, field, value, data)
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
return deleteByField(this.context)(entity, field, value)
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
return createMany(this.context)(entity, data)
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
return updateMany(this.context)(entity, filter, data)
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
return deleteMany(this.context)(entity, filter)
|
||||
}
|
||||
|
||||
async getCapabilities(): Promise<AdapterCapabilities> {
|
||||
return this.context.baseAdapter.getCapabilities()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.context.baseAdapter.close()
|
||||
}
|
||||
}
|
||||
|
||||
export type { User, ACLRule } from './acl/types'
|
||||
export { defaultACLRules } from './acl/default-rules'
|
||||
export { ACLAdapter } from './acl-adapter'
|
||||
export type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types'
|
||||
export { defaultACLRules } from '../acl/default-rules'
|
||||
|
||||
48
dbal/development/src/adapters/acl-adapter/read-strategy.ts
Normal file
48
dbal/development/src/adapters/acl-adapter/read-strategy.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { ListOptions, ListResult } from '../../core/foundation/types'
|
||||
import { enforceRowAccess, resolveOperation, withAudit } from './guards'
|
||||
import type { ACLContext } from './types'
|
||||
|
||||
export const createReadStrategy = (context: ACLContext) => {
|
||||
const read = async (entity: string, id: string): Promise<unknown | null> => {
|
||||
return withAudit(context, entity, 'read', async () => {
|
||||
const result = await context.baseAdapter.read(entity, id)
|
||||
if (result) {
|
||||
enforceRowAccess(context, entity, 'read', result as Record<string, unknown>)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const list = async (entity: string, options?: ListOptions): Promise<ListResult<unknown>> => {
|
||||
return withAudit(context, entity, 'list', () => context.baseAdapter.list(entity, options))
|
||||
}
|
||||
|
||||
const findFirst = async (entity: string, filter?: Record<string, unknown>): Promise<unknown | null> => {
|
||||
const operation = resolveOperation('findFirst')
|
||||
return withAudit(context, entity, operation, async () => {
|
||||
const result = await context.baseAdapter.findFirst(entity, filter)
|
||||
if (result) {
|
||||
enforceRowAccess(context, entity, operation, result as Record<string, unknown>)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const findByField = async (entity: string, field: string, value: unknown): Promise<unknown | null> => {
|
||||
const operation = resolveOperation('findByField')
|
||||
return withAudit(context, entity, operation, async () => {
|
||||
const result = await context.baseAdapter.findByField(entity, field, value)
|
||||
if (result) {
|
||||
enforceRowAccess(context, entity, operation, result as Record<string, unknown>)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
read,
|
||||
list,
|
||||
findFirst,
|
||||
findByField,
|
||||
}
|
||||
}
|
||||
27
dbal/development/src/adapters/acl-adapter/types.ts
Normal file
27
dbal/development/src/adapters/acl-adapter/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { DBALAdapter } from '../adapter'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
export interface ACLRule {
|
||||
entity: string
|
||||
roles: string[]
|
||||
operations: string[]
|
||||
rowLevelFilter?: (user: User, data: Record<string, unknown>) => boolean
|
||||
}
|
||||
|
||||
export interface ACLAdapterOptions {
|
||||
rules?: ACLRule[]
|
||||
auditLog?: boolean
|
||||
}
|
||||
|
||||
export interface ACLContext {
|
||||
baseAdapter: DBALAdapter
|
||||
user: User
|
||||
rules: ACLRule[]
|
||||
auditLog: boolean
|
||||
logger: (entity: string, operation: string, success: boolean, message?: string) => void
|
||||
}
|
||||
83
dbal/development/src/adapters/acl-adapter/write-strategy.ts
Normal file
83
dbal/development/src/adapters/acl-adapter/write-strategy.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { enforceRowAccess, resolveOperation, withAudit } from './guards'
|
||||
import type { ACLContext } from './types'
|
||||
|
||||
export const createWriteStrategy = (context: ACLContext) => {
|
||||
const create = async (entity: string, data: Record<string, unknown>): Promise<unknown> => {
|
||||
return withAudit(context, entity, 'create', () => context.baseAdapter.create(entity, data))
|
||||
}
|
||||
|
||||
const update = async (entity: string, id: string, data: Record<string, unknown>): Promise<unknown> => {
|
||||
return withAudit(context, entity, 'update', async () => {
|
||||
const existing = await context.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
enforceRowAccess(context, entity, 'update', existing as Record<string, unknown>)
|
||||
}
|
||||
return context.baseAdapter.update(entity, id, data)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = async (entity: string, id: string): Promise<boolean> => {
|
||||
return withAudit(context, entity, 'delete', async () => {
|
||||
const existing = await context.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
enforceRowAccess(context, entity, 'delete', existing as Record<string, unknown>)
|
||||
}
|
||||
return context.baseAdapter.delete(entity, id)
|
||||
})
|
||||
}
|
||||
|
||||
const upsert = async (
|
||||
entity: string,
|
||||
filter: Record<string, unknown>,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>,
|
||||
): Promise<unknown> => {
|
||||
return withAudit(context, entity, 'upsert', () => context.baseAdapter.upsert(entity, filter, createData, updateData))
|
||||
}
|
||||
|
||||
const updateByField = async (
|
||||
entity: string,
|
||||
field: string,
|
||||
value: unknown,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<unknown> => {
|
||||
const operation = resolveOperation('updateByField')
|
||||
return withAudit(context, entity, operation, () => context.baseAdapter.updateByField(entity, field, value, data))
|
||||
}
|
||||
|
||||
const deleteByField = async (entity: string, field: string, value: unknown): Promise<boolean> => {
|
||||
const operation = resolveOperation('deleteByField')
|
||||
return withAudit(context, entity, operation, () => context.baseAdapter.deleteByField(entity, field, value))
|
||||
}
|
||||
|
||||
const createMany = async (entity: string, data: Record<string, unknown>[]): Promise<number> => {
|
||||
const operation = resolveOperation('createMany')
|
||||
return withAudit(context, entity, operation, () => context.baseAdapter.createMany(entity, data))
|
||||
}
|
||||
|
||||
const updateMany = async (
|
||||
entity: string,
|
||||
filter: Record<string, unknown>,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<number> => {
|
||||
const operation = resolveOperation('updateMany')
|
||||
return withAudit(context, entity, operation, () => context.baseAdapter.updateMany(entity, filter, data))
|
||||
}
|
||||
|
||||
const deleteMany = async (entity: string, filter?: Record<string, unknown>): Promise<number> => {
|
||||
const operation = resolveOperation('deleteMany')
|
||||
return withAudit(context, entity, operation, () => context.baseAdapter.deleteMany(entity, filter))
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
update,
|
||||
delete: remove,
|
||||
upsert,
|
||||
updateByField,
|
||||
deleteByField,
|
||||
createMany,
|
||||
updateMany,
|
||||
deleteMany,
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description Audit logging for ACL operations
|
||||
*/
|
||||
|
||||
import type { User } from './types'
|
||||
import type { User } from '../acl-adapter/types'
|
||||
|
||||
/**
|
||||
* Log audit entry for ACL operation
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
import type { User, ACLRule } from './types'
|
||||
import type { ACLRule, User } from '../acl-adapter/types'
|
||||
|
||||
/**
|
||||
* Check if user has permission to perform operation on entity
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
import type { User, ACLRule } from './types'
|
||||
import type { ACLRule, User } from '../acl-adapter/types'
|
||||
|
||||
/**
|
||||
* Check row-level access for specific data
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description Default ACL rules for entities
|
||||
*/
|
||||
|
||||
import type { ACLRule } from './types'
|
||||
import type { ACLRule } from '../acl-adapter/types'
|
||||
|
||||
export const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user