Merge pull request #230 from johndoe6345789/codex/refactor-acl-adapter-structure-and-imports

Refactor ACL adapter into strategies
This commit is contained in:
2025-12-27 18:36:35 +00:00
committed by GitHub
12 changed files with 256 additions and 109 deletions

View File

@@ -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'

View 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'

View File

@@ -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

View File

@@ -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)

View File

@@ -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'

View 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,
}
}

View 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
}

View 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,
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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[] = [
{