mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +00:00
Compare commits
21 Commits
copilot/up
...
codex/bulk
| Author | SHA1 | Date | |
|---|---|---|---|
| 7173989234 | |||
| 227551a219 | |||
| 79238fda57 | |||
| d9f5a4ecc2 | |||
| 4cbd1f335e | |||
| 8acb8d8024 | |||
| eba50b5562 | |||
| c661b9cb6d | |||
| 919f8f2948 | |||
| d27436b9d6 | |||
| d718f3e455 | |||
|
|
97a4f9206a | ||
|
|
63bdb08bd2 | ||
|
|
a8ba66fce1 | ||
|
|
cf50c17b3f | ||
|
|
98c23b23fa | ||
|
|
f97e91b471 | ||
| c1d915f2ae | |||
|
|
672038938b | ||
|
|
aac7d1f4d4 | ||
|
|
d842d9c427 |
@@ -1,68 +1,16 @@
|
||||
import type { DBALAdapter, AdapterCapabilities } from '../adapters/adapter'
|
||||
/**
|
||||
* @file acl-adapter.ts
|
||||
* @description ACL adapter that wraps a base adapter with access control
|
||||
*/
|
||||
|
||||
import type { DBALAdapter, AdapterCapabilities } from './adapter'
|
||||
import type { ListOptions, ListResult } from '../core/foundation/types'
|
||||
import { DBALError } from '../core/foundation/errors'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
interface ACLRule {
|
||||
entity: string
|
||||
roles: string[]
|
||||
operations: string[]
|
||||
rowLevelFilter?: (user: User, data: Record<string, unknown>) => boolean
|
||||
}
|
||||
|
||||
const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['user'],
|
||||
operations: ['read', 'update'],
|
||||
rowLevelFilter: (user, data) => data.id === user.id
|
||||
},
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['user', 'admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
{
|
||||
entity: 'ComponentHierarchy',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Workflow',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'LuaScript',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
]
|
||||
import type { User, ACLRule } from './acl/types'
|
||||
import { resolvePermissionOperation } from './acl/resolve-permission-operation'
|
||||
import { checkPermission } from './acl/check-permission'
|
||||
import { checkRowLevelAccess } from './acl/check-row-level-access'
|
||||
import { logAudit } from './acl/audit-logger'
|
||||
import { defaultACLRules } from './acl/default-rules'
|
||||
|
||||
export class ACLAdapter implements DBALAdapter {
|
||||
private baseAdapter: DBALAdapter
|
||||
@@ -84,361 +32,214 @@ export class ACLAdapter implements DBALAdapter {
|
||||
this.auditLog = options?.auditLog ?? true
|
||||
}
|
||||
|
||||
private resolvePermissionOperation(operation: string): string {
|
||||
switch (operation) {
|
||||
case 'findFirst':
|
||||
case 'findByField':
|
||||
return 'read'
|
||||
case 'createMany':
|
||||
return 'create'
|
||||
case 'updateByField':
|
||||
case 'updateMany':
|
||||
return 'update'
|
||||
case 'deleteByField':
|
||||
case 'deleteMany':
|
||||
return 'delete'
|
||||
default:
|
||||
return operation
|
||||
private log(entity: string, operation: string, success: boolean, message?: string): void {
|
||||
if (this.auditLog) {
|
||||
logAudit(entity, operation, success, this.user, message)
|
||||
}
|
||||
}
|
||||
|
||||
private checkPermission(entity: string, operation: string): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation)
|
||||
)
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Permission denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`User ${this.user.username} (${this.user.role}) cannot ${operation} ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private checkRowLevelAccess(
|
||||
entity: string,
|
||||
operation: string,
|
||||
data: Record<string, unknown>
|
||||
): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation) &&
|
||||
rule.rowLevelFilter
|
||||
)
|
||||
|
||||
for (const rule of matchingRules) {
|
||||
if (rule.rowLevelFilter && !rule.rowLevelFilter(this.user, data)) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Row-level access denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`Row-level access denied for ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private logAudit(
|
||||
entity: string,
|
||||
operation: string,
|
||||
success: boolean,
|
||||
message?: string
|
||||
): void {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
user: this.user.username,
|
||||
userId: this.user.id,
|
||||
role: this.user.role,
|
||||
entity,
|
||||
operation,
|
||||
success,
|
||||
message
|
||||
}
|
||||
console.log('[DBAL Audit]', JSON.stringify(logEntry))
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'create')
|
||||
const operation = 'create'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.create(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
this.checkPermission(entity, 'read')
|
||||
const operation = 'read'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.read(entity, id)
|
||||
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, 'read', result as Record<string, unknown>)
|
||||
}
|
||||
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', true)
|
||||
checkRowLevelAccess(entity, operation, result as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'update')
|
||||
const operation = 'update'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
checkRowLevelAccess(entity, operation, existing as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.update(entity, id, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
this.checkPermission(entity, 'delete')
|
||||
const operation = 'delete'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'delete', existing as Record<string, unknown>)
|
||||
checkRowLevelAccess(entity, operation, existing as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.delete(entity, id)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
this.checkPermission(entity, 'list')
|
||||
const operation = 'list'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.list(entity, options)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findFirst')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('findFirst')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findFirst(entity, filter)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', true)
|
||||
checkRowLevelAccess(entity, resolvedOperation, result as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
this.log(entity, 'findFirst', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'findFirst', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('findByField')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', true)
|
||||
checkRowLevelAccess(entity, resolvedOperation, result as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
this.log(entity, 'findByField', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'findByField', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
filter: Record<string, unknown>,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
checkPermission(entity, 'create', this.user, this.rules, this.log.bind(this))
|
||||
checkPermission(entity, 'update', this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const existing = await this.baseAdapter.findByField(entity, uniqueField, uniqueValue)
|
||||
if (existing) {
|
||||
this.checkPermission(entity, 'update')
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
} else {
|
||||
this.checkPermission(entity, 'create')
|
||||
}
|
||||
|
||||
const result = await this.baseAdapter.upsert(entity, uniqueField, uniqueValue, createData, updateData)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', true)
|
||||
}
|
||||
const result = await this.baseAdapter.upsert(entity, filter, createData, updateData)
|
||||
this.log(entity, 'upsert', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'upsert', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('updateByField')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateByField(entity, field, value, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', true)
|
||||
}
|
||||
this.log(entity, 'updateByField', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'updateByField', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('deleteByField')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteByField(entity, field, value)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', true)
|
||||
}
|
||||
this.log(entity, 'deleteByField', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'deleteByField', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('createMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('createMany')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.createMany(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', true)
|
||||
}
|
||||
this.log(entity, 'createMany', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'createMany', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('updateMany')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateMany(entity, filter, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', true)
|
||||
}
|
||||
this.log(entity, 'updateMany', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'updateMany', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('deleteMany')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteMany(entity, filter)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', true)
|
||||
}
|
||||
this.log(entity, 'deleteMany', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'deleteMany', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -451,3 +252,7 @@ export class ACLAdapter implements DBALAdapter {
|
||||
await this.baseAdapter.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { User, ACLRule } from './acl/types'
|
||||
export { defaultACLRules } from './acl/default-rules'
|
||||
|
||||
453
dbal/development/src/adapters/acl-adapter.ts.backup
Normal file
453
dbal/development/src/adapters/acl-adapter.ts.backup
Normal file
@@ -0,0 +1,453 @@
|
||||
import type { DBALAdapter, AdapterCapabilities } from '../adapters/adapter'
|
||||
import type { ListOptions, ListResult } from '../core/foundation/types'
|
||||
import { DBALError } from '../core/foundation/errors'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
interface ACLRule {
|
||||
entity: string
|
||||
roles: string[]
|
||||
operations: string[]
|
||||
rowLevelFilter?: (user: User, data: Record<string, unknown>) => boolean
|
||||
}
|
||||
|
||||
const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['user'],
|
||||
operations: ['read', 'update'],
|
||||
rowLevelFilter: (user, data) => data.id === user.id
|
||||
},
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['user', 'admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
{
|
||||
entity: 'ComponentHierarchy',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Workflow',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'LuaScript',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
]
|
||||
|
||||
export class ACLAdapter implements DBALAdapter {
|
||||
private baseAdapter: DBALAdapter
|
||||
private user: User
|
||||
private rules: ACLRule[]
|
||||
private auditLog: boolean
|
||||
|
||||
constructor(
|
||||
baseAdapter: DBALAdapter,
|
||||
user: User,
|
||||
options?: {
|
||||
rules?: ACLRule[]
|
||||
auditLog?: boolean
|
||||
}
|
||||
) {
|
||||
this.baseAdapter = baseAdapter
|
||||
this.user = user
|
||||
this.rules = options?.rules || defaultACLRules
|
||||
this.auditLog = options?.auditLog ?? true
|
||||
}
|
||||
|
||||
private resolvePermissionOperation(operation: string): string {
|
||||
switch (operation) {
|
||||
case 'findFirst':
|
||||
case 'findByField':
|
||||
return 'read'
|
||||
case 'createMany':
|
||||
return 'create'
|
||||
case 'updateByField':
|
||||
case 'updateMany':
|
||||
return 'update'
|
||||
case 'deleteByField':
|
||||
case 'deleteMany':
|
||||
return 'delete'
|
||||
default:
|
||||
return operation
|
||||
}
|
||||
}
|
||||
|
||||
private checkPermission(entity: string, operation: string): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation)
|
||||
)
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Permission denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`User ${this.user.username} (${this.user.role}) cannot ${operation} ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private checkRowLevelAccess(
|
||||
entity: string,
|
||||
operation: string,
|
||||
data: Record<string, unknown>
|
||||
): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation) &&
|
||||
rule.rowLevelFilter
|
||||
)
|
||||
|
||||
for (const rule of matchingRules) {
|
||||
if (rule.rowLevelFilter && !rule.rowLevelFilter(this.user, data)) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Row-level access denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`Row-level access denied for ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private logAudit(
|
||||
entity: string,
|
||||
operation: string,
|
||||
success: boolean,
|
||||
message?: string
|
||||
): void {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
user: this.user.username,
|
||||
userId: this.user.id,
|
||||
role: this.user.role,
|
||||
entity,
|
||||
operation,
|
||||
success,
|
||||
message
|
||||
}
|
||||
console.log('[DBAL Audit]', JSON.stringify(logEntry))
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'create')
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.create(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
this.checkPermission(entity, 'read')
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.read(entity, id)
|
||||
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, 'read', result as Record<string, unknown>)
|
||||
}
|
||||
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'update')
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.update(entity, id, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
this.checkPermission(entity, 'delete')
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'delete', existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.delete(entity, id)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
this.checkPermission(entity, 'list')
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.list(entity, options)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findFirst')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findFirst(entity, filter)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
try {
|
||||
const existing = await this.baseAdapter.findByField(entity, uniqueField, uniqueValue)
|
||||
if (existing) {
|
||||
this.checkPermission(entity, 'update')
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
} else {
|
||||
this.checkPermission(entity, 'create')
|
||||
}
|
||||
|
||||
const result = await this.baseAdapter.upsert(entity, uniqueField, uniqueValue, createData, updateData)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateByField(entity, field, value, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteByField(entity, field, value)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('createMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.createMany(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateMany(entity, filter, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteMany(entity, filter)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getCapabilities(): Promise<AdapterCapabilities> {
|
||||
return this.baseAdapter.getCapabilities()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.baseAdapter.close()
|
||||
}
|
||||
}
|
||||
29
dbal/development/src/adapters/acl/audit-logger.ts
Normal file
29
dbal/development/src/adapters/acl/audit-logger.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file audit-logger.ts
|
||||
* @description Audit logging for ACL operations
|
||||
*/
|
||||
|
||||
import type { User } from './types'
|
||||
|
||||
/**
|
||||
* Log audit entry for ACL operation
|
||||
*/
|
||||
export const logAudit = (
|
||||
entity: string,
|
||||
operation: string,
|
||||
success: boolean,
|
||||
user: User,
|
||||
message?: string
|
||||
): void => {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
user: user.username,
|
||||
userId: user.id,
|
||||
role: user.role,
|
||||
entity,
|
||||
operation,
|
||||
success,
|
||||
message
|
||||
}
|
||||
console.log('[DBAL Audit]', JSON.stringify(logEntry))
|
||||
}
|
||||
34
dbal/development/src/adapters/acl/check-permission.ts
Normal file
34
dbal/development/src/adapters/acl/check-permission.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @file check-permission.ts
|
||||
* @description Check if user has permission for entity operation
|
||||
*/
|
||||
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
import type { User, ACLRule } from './types'
|
||||
|
||||
/**
|
||||
* Check if user has permission to perform operation on entity
|
||||
* @throws DBALError.forbidden if permission denied
|
||||
*/
|
||||
export const checkPermission = (
|
||||
entity: string,
|
||||
operation: string,
|
||||
user: User,
|
||||
rules: ACLRule[],
|
||||
logFn?: (entity: string, operation: string, success: boolean, message?: string) => void
|
||||
): void => {
|
||||
const matchingRules = rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(user.role) &&
|
||||
rule.operations.includes(operation)
|
||||
)
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
if (logFn) {
|
||||
logFn(entity, operation, false, 'Permission denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`User ${user.username} (${user.role}) cannot ${operation} ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
38
dbal/development/src/adapters/acl/check-row-level-access.ts
Normal file
38
dbal/development/src/adapters/acl/check-row-level-access.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @file check-row-level-access.ts
|
||||
* @description Check row-level access permissions
|
||||
*/
|
||||
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
import type { User, ACLRule } from './types'
|
||||
|
||||
/**
|
||||
* Check row-level access for specific data
|
||||
* @throws DBALError.forbidden if row-level access denied
|
||||
*/
|
||||
export const checkRowLevelAccess = (
|
||||
entity: string,
|
||||
operation: string,
|
||||
data: Record<string, unknown>,
|
||||
user: User,
|
||||
rules: ACLRule[],
|
||||
logFn?: (entity: string, operation: string, success: boolean, message?: string) => void
|
||||
): void => {
|
||||
const matchingRules = rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(user.role) &&
|
||||
rule.operations.includes(operation) &&
|
||||
rule.rowLevelFilter
|
||||
)
|
||||
|
||||
for (const rule of matchingRules) {
|
||||
if (rule.rowLevelFilter && !rule.rowLevelFilter(user, data)) {
|
||||
if (logFn) {
|
||||
logFn(entity, operation, false, 'Row-level access denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`Row-level access denied for ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
dbal/development/src/adapters/acl/default-rules.ts
Normal file
55
dbal/development/src/adapters/acl/default-rules.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @file default-rules.ts
|
||||
* @description Default ACL rules for entities
|
||||
*/
|
||||
|
||||
import type { ACLRule } from './types'
|
||||
|
||||
export const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['user'],
|
||||
operations: ['read', 'update'],
|
||||
rowLevelFilter: (user, data) => data.id === user.id
|
||||
},
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['user', 'admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
{
|
||||
entity: 'ComponentHierarchy',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Workflow',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'LuaScript',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file resolve-permission-operation.ts
|
||||
* @description Resolve DBAL operation to ACL permission operation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps complex DBAL operations to their base permission operations
|
||||
*/
|
||||
export const resolvePermissionOperation = (operation: string): string => {
|
||||
switch (operation) {
|
||||
case 'findFirst':
|
||||
case 'findByField':
|
||||
return 'read'
|
||||
case 'createMany':
|
||||
return 'create'
|
||||
case 'updateByField':
|
||||
case 'updateMany':
|
||||
return 'update'
|
||||
case 'deleteByField':
|
||||
case 'deleteMany':
|
||||
return 'delete'
|
||||
default:
|
||||
return operation
|
||||
}
|
||||
}
|
||||
17
dbal/development/src/adapters/acl/types.ts
Normal file
17
dbal/development/src/adapters/acl/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @file types.ts
|
||||
* @description Type definitions for ACL 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
|
||||
}
|
||||
20
dbal/development/src/bridges/utils/generate-request-id.ts
Normal file
20
dbal/development/src/bridges/utils/generate-request-id.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file generate-request-id.ts
|
||||
* @description Generate unique request ID for RPC calls
|
||||
*/
|
||||
|
||||
let requestIdCounter = 0
|
||||
|
||||
/**
|
||||
* Generate a unique request ID
|
||||
*/
|
||||
export const generateRequestId = (): string => {
|
||||
return `req_${Date.now()}_${++requestIdCounter}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the counter (useful for testing)
|
||||
*/
|
||||
export const resetRequestIdCounter = (): void => {
|
||||
requestIdCounter = 0
|
||||
}
|
||||
25
dbal/development/src/bridges/utils/rpc-types.ts
Normal file
25
dbal/development/src/bridges/utils/rpc-types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file rpc-types.ts
|
||||
* @description Type definitions for RPC messaging
|
||||
*/
|
||||
|
||||
export interface RPCMessage {
|
||||
id: string
|
||||
method: string
|
||||
params: unknown[]
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
id: string
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: number
|
||||
message: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface PendingRequest {
|
||||
resolve: (value: unknown) => void
|
||||
reject: (reason: unknown) => void
|
||||
}
|
||||
@@ -1,32 +1,19 @@
|
||||
/**
|
||||
* @file websocket-bridge.ts
|
||||
* @description WebSocket bridge adapter for remote DBAL daemon
|
||||
*/
|
||||
|
||||
import type { DBALAdapter, AdapterCapabilities } from '../adapters/adapter'
|
||||
import type { ListOptions, ListResult } from '../core/types'
|
||||
import { DBALError } from '../core/foundation/errors'
|
||||
|
||||
interface RPCMessage {
|
||||
id: string
|
||||
method: string
|
||||
params: unknown[]
|
||||
}
|
||||
|
||||
interface RPCResponse {
|
||||
id: string
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: number
|
||||
message: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
import { generateRequestId } from './utils/generate-request-id'
|
||||
import type { RPCMessage, RPCResponse, PendingRequest } from './utils/rpc-types'
|
||||
|
||||
export class WebSocketBridge implements DBALAdapter {
|
||||
private ws: WebSocket | null = null
|
||||
private endpoint: string
|
||||
private auth?: { user: unknown, session: unknown }
|
||||
private pendingRequests = new Map<string, {
|
||||
resolve: (value: unknown) => void
|
||||
reject: (reason: unknown) => void
|
||||
}>()
|
||||
private requestIdCounter = 0
|
||||
private pendingRequests = new Map<string, PendingRequest>()
|
||||
|
||||
constructor(endpoint: string, auth?: { user: unknown, session: unknown }) {
|
||||
this.endpoint = endpoint
|
||||
@@ -71,11 +58,12 @@ export class WebSocketBridge implements DBALAdapter {
|
||||
this.pendingRequests.delete(response.id)
|
||||
|
||||
if (response.error) {
|
||||
pending.reject(new DBALError(
|
||||
response.error.code,
|
||||
const error = new DBALError(
|
||||
response.error.message,
|
||||
response.error.code,
|
||||
response.error.details
|
||||
))
|
||||
)
|
||||
pending.reject(error)
|
||||
} else {
|
||||
pending.resolve(response.result)
|
||||
}
|
||||
@@ -87,7 +75,7 @@ export class WebSocketBridge implements DBALAdapter {
|
||||
private async call(method: string, ...params: unknown[]): Promise<unknown> {
|
||||
await this.connect()
|
||||
|
||||
const id = `req_${++this.requestIdCounter}`
|
||||
const id = generateRequestId()
|
||||
const message: RPCMessage = { id, method, params }
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -97,13 +85,13 @@ export class WebSocketBridge implements DBALAdapter {
|
||||
this.ws.send(JSON.stringify(message))
|
||||
} else {
|
||||
this.pendingRequests.delete(id)
|
||||
reject(DBALError.internal('WebSocket not connected'))
|
||||
reject(DBALError.internal('WebSocket connection not open'))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.pendingRequests.has(id)) {
|
||||
this.pendingRequests.delete(id)
|
||||
reject(DBALError.timeout('Request timeout'))
|
||||
reject(DBALError.timeout('Request timed out'))
|
||||
}
|
||||
}, 30000)
|
||||
})
|
||||
@@ -130,21 +118,20 @@ export class WebSocketBridge implements DBALAdapter {
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
return this.call('findFirst', entity, filter) as Promise<unknown | null>
|
||||
return this.call('findFirst', entity, filter)
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
return this.call('findByField', entity, field, value) as Promise<unknown | null>
|
||||
return this.call('findByField', entity, field, value)
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
filter: Record<string, unknown>,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
return this.call('upsert', entity, uniqueField, uniqueValue, createData, updateData)
|
||||
return this.call('upsert', entity, filter, createData, updateData)
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
|
||||
181
dbal/development/src/bridges/websocket-bridge.ts.backup
Normal file
181
dbal/development/src/bridges/websocket-bridge.ts.backup
Normal file
@@ -0,0 +1,181 @@
|
||||
import type { DBALAdapter, AdapterCapabilities } from '../adapters/adapter'
|
||||
import type { ListOptions, ListResult } from '../core/types'
|
||||
import { DBALError } from '../core/foundation/errors'
|
||||
|
||||
interface RPCMessage {
|
||||
id: string
|
||||
method: string
|
||||
params: unknown[]
|
||||
}
|
||||
|
||||
interface RPCResponse {
|
||||
id: string
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: number
|
||||
message: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketBridge implements DBALAdapter {
|
||||
private ws: WebSocket | null = null
|
||||
private endpoint: string
|
||||
private auth?: { user: unknown, session: unknown }
|
||||
private pendingRequests = new Map<string, {
|
||||
resolve: (value: unknown) => void
|
||||
reject: (reason: unknown) => void
|
||||
}>()
|
||||
private requestIdCounter = 0
|
||||
|
||||
constructor(endpoint: string, auth?: { user: unknown, session: unknown }) {
|
||||
this.endpoint = endpoint
|
||||
this.auth = auth
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new WebSocket(this.endpoint)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
resolve()
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
reject(DBALError.internal(`WebSocket connection failed: ${error}`))
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data)
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
try {
|
||||
const response: RPCResponse = JSON.parse(data)
|
||||
const pending = this.pendingRequests.get(response.id)
|
||||
|
||||
if (!pending) {
|
||||
return
|
||||
}
|
||||
|
||||
this.pendingRequests.delete(response.id)
|
||||
|
||||
if (response.error) {
|
||||
pending.reject(new DBALError(
|
||||
response.error.code,
|
||||
response.error.message,
|
||||
response.error.details
|
||||
))
|
||||
} else {
|
||||
pending.resolve(response.result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private async call(method: string, ...params: unknown[]): Promise<unknown> {
|
||||
await this.connect()
|
||||
|
||||
const id = `req_${++this.requestIdCounter}`
|
||||
const message: RPCMessage = { id, method, params }
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingRequests.set(id, { resolve, reject })
|
||||
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message))
|
||||
} else {
|
||||
this.pendingRequests.delete(id)
|
||||
reject(DBALError.internal('WebSocket not connected'))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.pendingRequests.has(id)) {
|
||||
this.pendingRequests.delete(id)
|
||||
reject(DBALError.timeout('Request timeout'))
|
||||
}
|
||||
}, 30000)
|
||||
})
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
return this.call('create', entity, data)
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
return this.call('read', entity, id)
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
return this.call('update', entity, id, data)
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
return this.call('delete', entity, id) as Promise<boolean>
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
return this.call('list', entity, options) as Promise<ListResult<unknown>>
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
return this.call('findFirst', entity, filter) as Promise<unknown | null>
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
return this.call('findByField', entity, field, value) as Promise<unknown | null>
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
return this.call('upsert', entity, uniqueField, uniqueValue, createData, updateData)
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
return this.call('updateByField', entity, field, value, data)
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
return this.call('deleteByField', entity, field, value) as Promise<boolean>
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
return this.call('deleteMany', entity, filter) as Promise<number>
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
return this.call('createMany', entity, data) as Promise<number>
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
return this.call('updateMany', entity, filter, data) as Promise<number>
|
||||
}
|
||||
|
||||
async getCapabilities(): Promise<AdapterCapabilities> {
|
||||
return this.call('getCapabilities') as Promise<AdapterCapabilities>
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
this.pendingRequests.clear()
|
||||
}
|
||||
}
|
||||
67
dbal/development/src/core/client/adapter-factory.ts
Normal file
67
dbal/development/src/core/client/adapter-factory.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @file adapter-factory.ts
|
||||
* @description Factory function for creating DBAL adapters based on configuration
|
||||
*/
|
||||
|
||||
import type { DBALConfig } from '../../runtime/config'
|
||||
import type { DBALAdapter } from '../../adapters/adapter'
|
||||
import { DBALError } from '../foundation/errors'
|
||||
import { PrismaAdapter, PostgresAdapter, MySQLAdapter } from '../../adapters/prisma-adapter'
|
||||
import { ACLAdapter } from '../../adapters/acl-adapter'
|
||||
import { WebSocketBridge } from '../../bridges/websocket-bridge'
|
||||
|
||||
/**
|
||||
* Creates the appropriate DBAL adapter based on configuration
|
||||
*/
|
||||
export const createAdapter = (config: DBALConfig): DBALAdapter => {
|
||||
let baseAdapter: DBALAdapter
|
||||
|
||||
if (config.mode === 'production' && config.endpoint) {
|
||||
baseAdapter = new WebSocketBridge(config.endpoint, config.auth)
|
||||
} else {
|
||||
switch (config.adapter) {
|
||||
case 'prisma':
|
||||
baseAdapter = new PrismaAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'postgres':
|
||||
baseAdapter = new PostgresAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'mysql':
|
||||
baseAdapter = new MySQLAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'sqlite':
|
||||
throw new Error('SQLite adapter to be implemented in Phase 3')
|
||||
case 'mongodb':
|
||||
throw new Error('MongoDB adapter to be implemented in Phase 3')
|
||||
default:
|
||||
throw DBALError.internal('Unknown adapter type')
|
||||
}
|
||||
}
|
||||
|
||||
if (config.auth?.user && config.security?.sandbox !== 'disabled') {
|
||||
return new ACLAdapter(
|
||||
baseAdapter,
|
||||
config.auth.user,
|
||||
{
|
||||
auditLog: config.security?.enableAuditLog ?? true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return baseAdapter
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* @file client.ts
|
||||
* @description Refactored DBAL Client using modular entity operations
|
||||
*
|
||||
* This is the streamlined client that delegates to entity-specific operation modules.
|
||||
*/
|
||||
|
||||
import type { DBALConfig } from '../runtime/config'
|
||||
import type { DBALAdapter } from '../adapters/adapter'
|
||||
import { DBALError } from './errors'
|
||||
import { PrismaAdapter, PostgresAdapter, MySQLAdapter } from '../adapters/prisma-adapter'
|
||||
import { ACLAdapter } from '../adapters/acl-adapter'
|
||||
import { WebSocketBridge } from '../bridges/websocket-bridge'
|
||||
import {
|
||||
createUserOperations,
|
||||
createPageOperations,
|
||||
createComponentOperations,
|
||||
createWorkflowOperations,
|
||||
createLuaScriptOperations,
|
||||
createPackageOperations,
|
||||
createSessionOperations,
|
||||
} from './entities'
|
||||
|
||||
/**
|
||||
* Create the appropriate adapter based on configuration
|
||||
*/
|
||||
const createAdapter = (config: DBALConfig): DBALAdapter => {
|
||||
let baseAdapter: DBALAdapter
|
||||
|
||||
if (config.mode === 'production' && config.endpoint) {
|
||||
baseAdapter = new WebSocketBridge(config.endpoint, config.auth)
|
||||
} else {
|
||||
switch (config.adapter) {
|
||||
case 'prisma':
|
||||
baseAdapter = new PrismaAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'postgres':
|
||||
baseAdapter = new PostgresAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'mysql':
|
||||
baseAdapter = new MySQLAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'sqlite':
|
||||
throw new Error('SQLite adapter to be implemented in Phase 3')
|
||||
case 'mongodb':
|
||||
throw new Error('MongoDB adapter to be implemented in Phase 3')
|
||||
default:
|
||||
throw DBALError.internal('Unknown adapter type')
|
||||
}
|
||||
}
|
||||
|
||||
if (config.auth?.user && config.security?.sandbox !== 'disabled') {
|
||||
return new ACLAdapter(
|
||||
baseAdapter,
|
||||
config.auth.user,
|
||||
{
|
||||
auditLog: config.security?.enableAuditLog ?? true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return baseAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* DBAL Client - Main interface for database operations
|
||||
*
|
||||
* Provides CRUD operations for all entities through modular operation handlers.
|
||||
* Each entity type has its own dedicated operations module following the
|
||||
* single-responsibility pattern.
|
||||
*/
|
||||
export class DBALClient {
|
||||
private adapter: DBALAdapter
|
||||
private config: DBALConfig
|
||||
|
||||
constructor(config: DBALConfig) {
|
||||
this.config = config
|
||||
|
||||
// Validate configuration
|
||||
if (!config.adapter) {
|
||||
throw new Error('Adapter type must be specified')
|
||||
}
|
||||
if (config.mode !== 'production' && !config.database?.url) {
|
||||
throw new Error('Database URL must be specified for non-production mode')
|
||||
}
|
||||
|
||||
this.adapter = createAdapter(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* User entity operations
|
||||
*/
|
||||
get users() {
|
||||
return createUserOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Page entity operations
|
||||
*/
|
||||
get pages() {
|
||||
return createPageOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Component hierarchy entity operations
|
||||
*/
|
||||
get components() {
|
||||
return createComponentOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow entity operations
|
||||
*/
|
||||
get workflows() {
|
||||
return createWorkflowOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lua script entity operations
|
||||
*/
|
||||
get luaScripts() {
|
||||
return createLuaScriptOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Package entity operations
|
||||
*/
|
||||
get packages() {
|
||||
return createPackageOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Session entity operations
|
||||
*/
|
||||
get sessions() {
|
||||
return createSessionOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get adapter capabilities
|
||||
*/
|
||||
async capabilities() {
|
||||
return this.adapter.getCapabilities()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the client connection
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
await this.adapter.close()
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,24 @@
|
||||
/**
|
||||
* @file client.ts
|
||||
* @description DBAL Client - Main interface for database operations
|
||||
*
|
||||
* Provides CRUD operations for all entities through modular operation handlers.
|
||||
* Each entity type has its own dedicated operations module following the
|
||||
* single-responsibility pattern.
|
||||
*/
|
||||
|
||||
import type { DBALConfig } from '../../runtime/config'
|
||||
import type { DBALAdapter } from '../../adapters/adapter'
|
||||
import type { User, PageView, ComponentHierarchy, Workflow, LuaScript, Package, Session, ListOptions, ListResult } from '../foundation/types'
|
||||
import { DBALError } from '../foundation/errors'
|
||||
import { PrismaAdapter, PostgresAdapter, MySQLAdapter } from '../../adapters/prisma-adapter'
|
||||
import { ACLAdapter } from '../../adapters/acl-adapter'
|
||||
import { WebSocketBridge } from '../../bridges/websocket-bridge'
|
||||
import { createAdapter } from './adapter-factory'
|
||||
import {
|
||||
validateUserCreate,
|
||||
validateUserUpdate,
|
||||
validatePageCreate,
|
||||
validatePageUpdate,
|
||||
validateComponentHierarchyCreate,
|
||||
validateComponentHierarchyUpdate,
|
||||
validateWorkflowCreate,
|
||||
validateWorkflowUpdate,
|
||||
validateLuaScriptCreate,
|
||||
validateLuaScriptUpdate,
|
||||
validatePackageCreate,
|
||||
validatePackageUpdate,
|
||||
validateSessionCreate,
|
||||
validateSessionUpdate,
|
||||
validateId,
|
||||
} from '../validation'
|
||||
createUserOperations,
|
||||
createPageOperations,
|
||||
createComponentOperations,
|
||||
createWorkflowOperations,
|
||||
createLuaScriptOperations,
|
||||
createPackageOperations,
|
||||
createSessionOperations,
|
||||
} from '../entities'
|
||||
|
||||
export class DBALClient {
|
||||
private adapter: DBALAdapter
|
||||
@@ -38,775 +35,68 @@ export class DBALClient {
|
||||
throw new Error('Database URL must be specified for non-production mode')
|
||||
}
|
||||
|
||||
this.adapter = this.createAdapter(config)
|
||||
}
|
||||
|
||||
private createAdapter(config: DBALConfig): DBALAdapter {
|
||||
let baseAdapter: DBALAdapter
|
||||
|
||||
if (config.mode === 'production' && config.endpoint) {
|
||||
baseAdapter = new WebSocketBridge(config.endpoint, config.auth)
|
||||
} else {
|
||||
switch (config.adapter) {
|
||||
case 'prisma':
|
||||
baseAdapter = new PrismaAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'postgres':
|
||||
baseAdapter = new PostgresAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'mysql':
|
||||
baseAdapter = new MySQLAdapter(
|
||||
config.database?.url,
|
||||
{
|
||||
queryTimeout: config.performance?.queryTimeout
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'sqlite':
|
||||
throw new Error('SQLite adapter to be implemented in Phase 3')
|
||||
case 'mongodb':
|
||||
throw new Error('MongoDB adapter to be implemented in Phase 3')
|
||||
default:
|
||||
throw DBALError.internal('Unknown adapter type')
|
||||
}
|
||||
}
|
||||
|
||||
if (config.auth?.user && config.security?.sandbox !== 'disabled') {
|
||||
return new ACLAdapter(
|
||||
baseAdapter,
|
||||
config.auth.user,
|
||||
{
|
||||
auditLog: config.security?.enableAuditLog ?? true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return baseAdapter
|
||||
this.adapter = createAdapter(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* User entity operations
|
||||
*/
|
||||
get users() {
|
||||
return {
|
||||
create: async (data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> => {
|
||||
// Validate input
|
||||
const validationErrors = validateUserCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user data',
|
||||
validationErrors.map(error => ({ field: 'user', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('User', data) as Promise<User>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique constraints)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`User with username or email already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<User | null> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('User', id) as User | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`User not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<User>): Promise<User> => {
|
||||
// Validate ID
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
// Validate update data
|
||||
const validationErrors = validateUserUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user update data',
|
||||
validationErrors.map(error => ({ field: 'user', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('User', id, data) as Promise<User>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique constraints)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Username or email already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('User', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`User not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<User>> => {
|
||||
return this.adapter.list('User', options) as Promise<ListResult<User>>
|
||||
},
|
||||
createMany: async (data: Array<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>): Promise<number> => {
|
||||
if (!data || data.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const validationErrors = data.flatMap((item, index) =>
|
||||
validateUserCreate(item).map(error => ({ field: `users[${index}]`, error }))
|
||||
)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid user batch', validationErrors)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.createMany('User', data as Record<string, unknown>[])
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Username or email already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
updateMany: async (filter: Record<string, unknown>, data: Partial<User>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires data', [
|
||||
{ field: 'data', error: 'Update data is required' },
|
||||
])
|
||||
}
|
||||
|
||||
const validationErrors = validateUserUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user update data',
|
||||
validationErrors.map(error => ({ field: 'user', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.updateMany('User', filter, data as Record<string, unknown>)
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Username or email already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
deleteMany: async (filter: Record<string, unknown>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk delete requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
return this.adapter.deleteMany('User', filter)
|
||||
},
|
||||
}
|
||||
return createUserOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Page entity operations
|
||||
*/
|
||||
get pages() {
|
||||
return {
|
||||
create: async (data: Omit<PageView, 'id' | 'createdAt' | 'updatedAt'>): Promise<PageView> => {
|
||||
// Validate input
|
||||
const validationErrors = validatePageCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page data',
|
||||
validationErrors.map(error => ({ field: 'page', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('PageView', data) as Promise<PageView>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique slug)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Page with slug '${data.slug}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<PageView | null> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('PageView', id) as PageView | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
readBySlug: async (slug: string): Promise<PageView | null> => {
|
||||
// Validate slug
|
||||
if (!slug || slug.trim().length === 0) {
|
||||
throw DBALError.validationError('Slug cannot be empty', [
|
||||
{ field: 'slug', error: 'Slug is required' }
|
||||
])
|
||||
}
|
||||
|
||||
const result = await this.adapter.list('PageView', { filter: { slug } })
|
||||
if (result.data.length === 0) {
|
||||
throw DBALError.notFound(`Page not found with slug: ${slug}`)
|
||||
}
|
||||
return result.data[0] as PageView
|
||||
},
|
||||
update: async (id: string, data: Partial<PageView>): Promise<PageView> => {
|
||||
// Validate ID
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
// Validate update data
|
||||
const validationErrors = validatePageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page update data',
|
||||
validationErrors.map(error => ({ field: 'page', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('PageView', id, data) as Promise<PageView>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique slug)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Slug already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('PageView', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<PageView>> => {
|
||||
return this.adapter.list('PageView', options) as Promise<ListResult<PageView>>
|
||||
},
|
||||
}
|
||||
return createPageOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Component hierarchy entity operations
|
||||
*/
|
||||
get components() {
|
||||
return {
|
||||
create: async (data: Omit<ComponentHierarchy, 'id' | 'createdAt' | 'updatedAt'>): Promise<ComponentHierarchy> => {
|
||||
const validationErrors = validateComponentHierarchyCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component data',
|
||||
validationErrors.map(error => ({ field: 'component', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.create('ComponentHierarchy', data) as Promise<ComponentHierarchy>
|
||||
},
|
||||
read: async (id: string): Promise<ComponentHierarchy | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.read('ComponentHierarchy', id) as Promise<ComponentHierarchy | null>
|
||||
},
|
||||
update: async (id: string, data: Partial<ComponentHierarchy>): Promise<ComponentHierarchy> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateComponentHierarchyUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component update data',
|
||||
validationErrors.map(error => ({ field: 'component', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.update('ComponentHierarchy', id, data) as Promise<ComponentHierarchy>
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.delete('ComponentHierarchy', id)
|
||||
},
|
||||
getTree: async (pageId: string): Promise<ComponentHierarchy[]> => {
|
||||
const validationErrors = validateId(pageId)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
validationErrors.map(error => ({ field: 'pageId', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.list('ComponentHierarchy', { filter: { pageId } })
|
||||
return result.data as ComponentHierarchy[]
|
||||
},
|
||||
}
|
||||
return createComponentOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow entity operations
|
||||
*/
|
||||
get workflows() {
|
||||
return {
|
||||
create: async (data: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> => {
|
||||
const validationErrors = validateWorkflowCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow data',
|
||||
validationErrors.map(error => ({ field: 'workflow', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('Workflow', data) as Promise<Workflow>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Workflow with name '${data.name}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<Workflow | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('Workflow', id) as Workflow | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Workflow not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<Workflow>): Promise<Workflow> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateWorkflowUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow update data',
|
||||
validationErrors.map(error => ({ field: 'workflow', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('Workflow', id, data) as Promise<Workflow>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Workflow name already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('Workflow', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Workflow not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<Workflow>> => {
|
||||
return this.adapter.list('Workflow', options) as Promise<ListResult<Workflow>>
|
||||
},
|
||||
}
|
||||
return createWorkflowOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lua script entity operations
|
||||
*/
|
||||
get luaScripts() {
|
||||
return {
|
||||
create: async (data: Omit<LuaScript, 'id' | 'createdAt' | 'updatedAt'>): Promise<LuaScript> => {
|
||||
const validationErrors = validateLuaScriptCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script data',
|
||||
validationErrors.map(error => ({ field: 'luaScript', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('LuaScript', data) as Promise<LuaScript>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Lua script with name '${data.name}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<LuaScript | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('LuaScript', id) as LuaScript | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Lua script not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<LuaScript>): Promise<LuaScript> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateLuaScriptUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script update data',
|
||||
validationErrors.map(error => ({ field: 'luaScript', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('LuaScript', id, data) as Promise<LuaScript>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Lua script name already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('LuaScript', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Lua script not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<LuaScript>> => {
|
||||
return this.adapter.list('LuaScript', options) as Promise<ListResult<LuaScript>>
|
||||
},
|
||||
}
|
||||
return createLuaScriptOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Package entity operations
|
||||
*/
|
||||
get packages() {
|
||||
return {
|
||||
create: async (data: Omit<Package, 'id' | 'createdAt' | 'updatedAt'>): Promise<Package> => {
|
||||
const validationErrors = validatePackageCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package data',
|
||||
validationErrors.map(error => ({ field: 'package', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('Package', data) as Promise<Package>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Package ${data.name}@${data.version} already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<Package | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('Package', id) as Package | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Package not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<Package>): Promise<Package> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validatePackageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package update data',
|
||||
validationErrors.map(error => ({ field: 'package', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('Package', id, data) as Promise<Package>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('Package', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Package not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<Package>> => {
|
||||
return this.adapter.list('Package', options) as Promise<ListResult<Package>>
|
||||
},
|
||||
createMany: async (data: Array<Omit<Package, 'id' | 'createdAt' | 'updatedAt'>>): Promise<number> => {
|
||||
if (!data || data.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const validationErrors = data.flatMap((item, index) =>
|
||||
validatePackageCreate(item).map(error => ({ field: `packages[${index}]`, error }))
|
||||
)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package batch', validationErrors)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.createMany('Package', data as Record<string, unknown>[])
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
updateMany: async (filter: Record<string, unknown>, data: Partial<Package>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires data', [
|
||||
{ field: 'data', error: 'Update data is required' },
|
||||
])
|
||||
}
|
||||
|
||||
const validationErrors = validatePackageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package update data',
|
||||
validationErrors.map(error => ({ field: 'package', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.updateMany('Package', filter, data as Record<string, unknown>)
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
deleteMany: async (filter: Record<string, unknown>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk delete requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
return this.adapter.deleteMany('Package', filter)
|
||||
},
|
||||
}
|
||||
return createPackageOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Session entity operations
|
||||
*/
|
||||
get sessions() {
|
||||
return {
|
||||
create: async (data: Omit<Session, 'id' | 'createdAt' | 'lastActivity'>): Promise<Session> => {
|
||||
const validationErrors = validateSessionCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session data',
|
||||
validationErrors.map(error => ({ field: 'session', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('Session', data) as Promise<Session>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Session token already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<Session | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('Session', id) as Session | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Session not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<Session>): Promise<Session> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateSessionUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session update data',
|
||||
validationErrors.map(error => ({ field: 'session', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('Session', id, data) as Promise<Session>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Session token already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('Session', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Session not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<Session>> => {
|
||||
return this.adapter.list('Session', options) as Promise<ListResult<Session>>
|
||||
},
|
||||
}
|
||||
return createSessionOperations(this.adapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get adapter capabilities
|
||||
*/
|
||||
async capabilities() {
|
||||
return this.adapter.getCapabilities()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the client connection
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
await this.adapter.close()
|
||||
}
|
||||
|
||||
757
dbal/development/src/core/client/client.ts.backup
Normal file
757
dbal/development/src/core/client/client.ts.backup
Normal file
@@ -0,0 +1,757 @@
|
||||
/**
|
||||
* @file client.ts
|
||||
* @description DBAL Client - Main interface for database operations
|
||||
*
|
||||
* Provides CRUD operations for all entities through modular operation handlers.
|
||||
* Each entity type has its own dedicated operations module following the
|
||||
* single-responsibility pattern.
|
||||
*/
|
||||
|
||||
import type { DBALConfig } from '../../runtime/config'
|
||||
import type { DBALAdapter } from '../../adapters/adapter'
|
||||
import { createAdapter } from './adapter-factory'
|
||||
import {
|
||||
createUserOperations,
|
||||
createPageOperations,
|
||||
createComponentOperations,
|
||||
createWorkflowOperations,
|
||||
createLuaScriptOperations,
|
||||
createPackageOperations,
|
||||
createSessionOperations,
|
||||
} from '../entities'
|
||||
|
||||
export class DBALClient {
|
||||
private adapter: DBALAdapter
|
||||
private config: DBALConfig
|
||||
|
||||
constructor(config: DBALConfig) {
|
||||
this.config = config
|
||||
|
||||
// Validate configuration
|
||||
if (!config.adapter) {
|
||||
throw new Error('Adapter type must be specified')
|
||||
}
|
||||
if (config.mode !== 'production' && !config.database?.url) {
|
||||
throw new Error('Database URL must be specified for non-production mode')
|
||||
}
|
||||
|
||||
this.adapter = createAdapter(config)
|
||||
}
|
||||
|
||||
get users() {
|
||||
return {
|
||||
create: async (data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> => {
|
||||
// Validate input
|
||||
const validationErrors = validateUserCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user data',
|
||||
validationErrors.map(error => ({ field: 'user', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('User', data) as Promise<User>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique constraints)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`User with username or email already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<User | null> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('User', id) as User | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`User not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<User>): Promise<User> => {
|
||||
// Validate ID
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
// Validate update data
|
||||
const validationErrors = validateUserUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user update data',
|
||||
validationErrors.map(error => ({ field: 'user', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('User', id, data) as Promise<User>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique constraints)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Username or email already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('User', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`User not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<User>> => {
|
||||
return this.adapter.list('User', options) as Promise<ListResult<User>>
|
||||
},
|
||||
createMany: async (data: Array<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>): Promise<number> => {
|
||||
if (!data || data.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const validationErrors = data.flatMap((item, index) =>
|
||||
validateUserCreate(item).map(error => ({ field: `users[${index}]`, error }))
|
||||
)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid user batch', validationErrors)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.createMany('User', data as Record<string, unknown>[])
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Username or email already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
updateMany: async (filter: Record<string, unknown>, data: Partial<User>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires data', [
|
||||
{ field: 'data', error: 'Update data is required' },
|
||||
])
|
||||
}
|
||||
|
||||
const validationErrors = validateUserUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid user update data',
|
||||
validationErrors.map(error => ({ field: 'user', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.updateMany('User', filter, data as Record<string, unknown>)
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Username or email already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
deleteMany: async (filter: Record<string, unknown>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk delete requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
return this.adapter.deleteMany('User', filter)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get pages() {
|
||||
return {
|
||||
create: async (data: Omit<PageView, 'id' | 'createdAt' | 'updatedAt'>): Promise<PageView> => {
|
||||
// Validate input
|
||||
const validationErrors = validatePageCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page data',
|
||||
validationErrors.map(error => ({ field: 'page', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('PageView', data) as Promise<PageView>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique slug)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Page with slug '${data.slug}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<PageView | null> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('PageView', id) as PageView | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
readBySlug: async (slug: string): Promise<PageView | null> => {
|
||||
// Validate slug
|
||||
if (!slug || slug.trim().length === 0) {
|
||||
throw DBALError.validationError('Slug cannot be empty', [
|
||||
{ field: 'slug', error: 'Slug is required' }
|
||||
])
|
||||
}
|
||||
|
||||
const result = await this.adapter.list('PageView', { filter: { slug } })
|
||||
if (result.data.length === 0) {
|
||||
throw DBALError.notFound(`Page not found with slug: ${slug}`)
|
||||
}
|
||||
return result.data[0] as PageView
|
||||
},
|
||||
update: async (id: string, data: Partial<PageView>): Promise<PageView> => {
|
||||
// Validate ID
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
// Validate update data
|
||||
const validationErrors = validatePageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page update data',
|
||||
validationErrors.map(error => ({ field: 'page', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('PageView', id, data) as Promise<PageView>
|
||||
} catch (error) {
|
||||
// Check for conflict errors (unique slug)
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Slug already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
// Validate ID
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('PageView', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<PageView>> => {
|
||||
return this.adapter.list('PageView', options) as Promise<ListResult<PageView>>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get components() {
|
||||
return {
|
||||
create: async (data: Omit<ComponentHierarchy, 'id' | 'createdAt' | 'updatedAt'>): Promise<ComponentHierarchy> => {
|
||||
const validationErrors = validateComponentHierarchyCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component data',
|
||||
validationErrors.map(error => ({ field: 'component', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.create('ComponentHierarchy', data) as Promise<ComponentHierarchy>
|
||||
},
|
||||
read: async (id: string): Promise<ComponentHierarchy | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.read('ComponentHierarchy', id) as Promise<ComponentHierarchy | null>
|
||||
},
|
||||
update: async (id: string, data: Partial<ComponentHierarchy>): Promise<ComponentHierarchy> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateComponentHierarchyUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component update data',
|
||||
validationErrors.map(error => ({ field: 'component', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.update('ComponentHierarchy', id, data) as Promise<ComponentHierarchy>
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid component ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
return this.adapter.delete('ComponentHierarchy', id)
|
||||
},
|
||||
getTree: async (pageId: string): Promise<ComponentHierarchy[]> => {
|
||||
const validationErrors = validateId(pageId)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid page ID',
|
||||
validationErrors.map(error => ({ field: 'pageId', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.list('ComponentHierarchy', { filter: { pageId } })
|
||||
return result.data as ComponentHierarchy[]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get workflows() {
|
||||
return {
|
||||
create: async (data: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> => {
|
||||
const validationErrors = validateWorkflowCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow data',
|
||||
validationErrors.map(error => ({ field: 'workflow', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('Workflow', data) as Promise<Workflow>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Workflow with name '${data.name}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<Workflow | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('Workflow', id) as Workflow | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Workflow not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<Workflow>): Promise<Workflow> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateWorkflowUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow update data',
|
||||
validationErrors.map(error => ({ field: 'workflow', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('Workflow', id, data) as Promise<Workflow>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Workflow name already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid workflow ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('Workflow', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Workflow not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<Workflow>> => {
|
||||
return this.adapter.list('Workflow', options) as Promise<ListResult<Workflow>>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get luaScripts() {
|
||||
return {
|
||||
create: async (data: Omit<LuaScript, 'id' | 'createdAt' | 'updatedAt'>): Promise<LuaScript> => {
|
||||
const validationErrors = validateLuaScriptCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script data',
|
||||
validationErrors.map(error => ({ field: 'luaScript', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('LuaScript', data) as Promise<LuaScript>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Lua script with name '${data.name}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<LuaScript | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('LuaScript', id) as LuaScript | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Lua script not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<LuaScript>): Promise<LuaScript> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateLuaScriptUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script update data',
|
||||
validationErrors.map(error => ({ field: 'luaScript', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('LuaScript', id, data) as Promise<LuaScript>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Lua script name already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid Lua script ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('LuaScript', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Lua script not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<LuaScript>> => {
|
||||
return this.adapter.list('LuaScript', options) as Promise<ListResult<LuaScript>>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get packages() {
|
||||
return {
|
||||
create: async (data: Omit<Package, 'id' | 'createdAt' | 'updatedAt'>): Promise<Package> => {
|
||||
const validationErrors = validatePackageCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package data',
|
||||
validationErrors.map(error => ({ field: 'package', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('Package', data) as Promise<Package>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Package ${data.name}@${data.version} already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<Package | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('Package', id) as Package | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Package not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<Package>): Promise<Package> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validatePackageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package update data',
|
||||
validationErrors.map(error => ({ field: 'package', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('Package', id, data) as Promise<Package>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('Package', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Package not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<Package>> => {
|
||||
return this.adapter.list('Package', options) as Promise<ListResult<Package>>
|
||||
},
|
||||
createMany: async (data: Array<Omit<Package, 'id' | 'createdAt' | 'updatedAt'>>): Promise<number> => {
|
||||
if (!data || data.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const validationErrors = data.flatMap((item, index) =>
|
||||
validatePackageCreate(item).map(error => ({ field: `packages[${index}]`, error }))
|
||||
)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package batch', validationErrors)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.createMany('Package', data as Record<string, unknown>[])
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
updateMany: async (filter: Record<string, unknown>, data: Partial<Package>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires data', [
|
||||
{ field: 'data', error: 'Update data is required' },
|
||||
])
|
||||
}
|
||||
|
||||
const validationErrors = validatePackageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid package update data',
|
||||
validationErrors.map(error => ({ field: 'package', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.updateMany('Package', filter, data as Record<string, unknown>)
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
deleteMany: async (filter: Record<string, unknown>): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk delete requires a filter', [
|
||||
{ field: 'filter', error: 'Filter is required' },
|
||||
])
|
||||
}
|
||||
|
||||
return this.adapter.deleteMany('Package', filter)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get sessions() {
|
||||
return {
|
||||
create: async (data: Omit<Session, 'id' | 'createdAt' | 'lastActivity'>): Promise<Session> => {
|
||||
const validationErrors = validateSessionCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session data',
|
||||
validationErrors.map(error => ({ field: 'session', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.create('Session', data) as Promise<Session>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Session token already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
read: async (id: string): Promise<Session | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.read('Session', id) as Session | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Session not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
update: async (id: string, data: Partial<Session>): Promise<Session> => {
|
||||
const idErrors = validateId(id)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session ID',
|
||||
idErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const validationErrors = validateSessionUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session update data',
|
||||
validationErrors.map(error => ({ field: 'session', error }))
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return this.adapter.update('Session', id, data) as Promise<Session>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Session token already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
delete: async (id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError(
|
||||
'Invalid session ID',
|
||||
validationErrors.map(error => ({ field: 'id', error }))
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.adapter.delete('Session', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Session not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
list: async (options?: ListOptions): Promise<ListResult<Session>> => {
|
||||
return this.adapter.list('Session', options) as Promise<ListResult<Session>>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async capabilities() {
|
||||
return this.adapter.getCapabilities()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.adapter.close()
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,13 @@
|
||||
/**
|
||||
* Multi-Tenant Context and Identity Management
|
||||
*
|
||||
* Provides tenant isolation, access control, and quota management
|
||||
* for both blob storage and structured data.
|
||||
* @file tenant-context.ts
|
||||
* @description Multi-tenant context and identity management
|
||||
*/
|
||||
|
||||
export interface TenantIdentity {
|
||||
tenantId: string
|
||||
userId: string
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer'
|
||||
permissions: Set<string>
|
||||
}
|
||||
import type { TenantIdentity, TenantQuota, TenantContext } from './tenant/tenant-types'
|
||||
import * as PermissionChecks from './tenant/permission-checks'
|
||||
import * as QuotaChecks from './tenant/quota-checks'
|
||||
|
||||
export interface TenantQuota {
|
||||
// Blob storage quotas
|
||||
maxBlobStorageBytes?: number
|
||||
maxBlobCount?: number
|
||||
maxBlobSizeBytes?: number
|
||||
|
||||
// Structured data quotas
|
||||
maxRecords?: number
|
||||
maxDataSizeBytes?: number
|
||||
maxListLength?: number
|
||||
|
||||
// Computed usage
|
||||
currentBlobStorageBytes: number
|
||||
currentBlobCount: number
|
||||
currentRecords: number
|
||||
currentDataSizeBytes: number
|
||||
}
|
||||
|
||||
export interface TenantContext {
|
||||
identity: TenantIdentity
|
||||
quota: TenantQuota
|
||||
namespace: string // For blob storage isolation
|
||||
|
||||
// Check if operation is allowed
|
||||
canRead(resource: string): boolean
|
||||
canWrite(resource: string): boolean
|
||||
canDelete(resource: string): boolean
|
||||
|
||||
// Check quota availability
|
||||
canUploadBlob(sizeBytes: number): boolean
|
||||
canCreateRecord(): boolean
|
||||
canAddToList(additionalItems: number): boolean
|
||||
}
|
||||
export type { TenantIdentity, TenantQuota, TenantContext }
|
||||
|
||||
export class DefaultTenantContext implements TenantContext {
|
||||
constructor(
|
||||
@@ -54,202 +17,38 @@ export class DefaultTenantContext implements TenantContext {
|
||||
) {}
|
||||
|
||||
canRead(resource: string): boolean {
|
||||
// Owner and admin can read everything
|
||||
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check specific permissions
|
||||
return (
|
||||
this.identity.permissions.has('read:*') ||
|
||||
this.identity.permissions.has(`read:${resource}`)
|
||||
)
|
||||
return PermissionChecks.canRead(this.identity, resource)
|
||||
}
|
||||
|
||||
canWrite(resource: string): boolean {
|
||||
// Only owner and admin can write
|
||||
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check specific permissions
|
||||
return (
|
||||
this.identity.permissions.has('write:*') ||
|
||||
this.identity.permissions.has(`write:${resource}`)
|
||||
)
|
||||
return PermissionChecks.canWrite(this.identity, resource)
|
||||
}
|
||||
|
||||
canDelete(resource: string): boolean {
|
||||
// Only owner and admin can delete
|
||||
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check specific permissions
|
||||
return (
|
||||
this.identity.permissions.has('delete:*') ||
|
||||
this.identity.permissions.has(`delete:${resource}`)
|
||||
)
|
||||
return PermissionChecks.canDelete(this.identity, resource)
|
||||
}
|
||||
|
||||
canUploadBlob(sizeBytes: number): boolean {
|
||||
const { quota } = this
|
||||
|
||||
// Check max blob size
|
||||
if (quota.maxBlobSizeBytes && sizeBytes > quota.maxBlobSizeBytes) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check total storage quota
|
||||
if (quota.maxBlobStorageBytes) {
|
||||
if (quota.currentBlobStorageBytes + sizeBytes > quota.maxBlobStorageBytes) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check blob count quota
|
||||
if (quota.maxBlobCount) {
|
||||
if (quota.currentBlobCount >= quota.maxBlobCount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return QuotaChecks.canUploadBlob(this.quota, sizeBytes)
|
||||
}
|
||||
|
||||
canCreateRecord(): boolean {
|
||||
const { quota } = this
|
||||
|
||||
if (quota.maxRecords) {
|
||||
return quota.currentRecords < quota.maxRecords
|
||||
}
|
||||
|
||||
return true
|
||||
return QuotaChecks.canCreateRecord(this.quota)
|
||||
}
|
||||
|
||||
canAddToList(additionalItems: number): boolean {
|
||||
const { quota } = this
|
||||
|
||||
if (quota.maxListLength && additionalItems > quota.maxListLength) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return QuotaChecks.canAddToList(this.quota, additionalItems)
|
||||
}
|
||||
}
|
||||
|
||||
export interface TenantManager {
|
||||
// Get tenant context for operations
|
||||
getTenantContext(tenantId: string, userId: string): Promise<TenantContext>
|
||||
|
||||
// Update quota usage
|
||||
updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void>
|
||||
updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void>
|
||||
|
||||
// Create/update tenant
|
||||
createTenant(tenantId: string, quota?: Partial<TenantQuota>): Promise<void>
|
||||
updateQuota(tenantId: string, quota: Partial<TenantQuota>): Promise<void>
|
||||
|
||||
// Get current usage
|
||||
getUsage(tenantId: string): Promise<TenantQuota>
|
||||
}
|
||||
|
||||
export class InMemoryTenantManager implements TenantManager {
|
||||
private tenants = new Map<string, TenantQuota>()
|
||||
private permissions = new Map<string, TenantIdentity>()
|
||||
|
||||
async getTenantContext(tenantId: string, userId: string): Promise<TenantContext> {
|
||||
let quota = this.tenants.get(tenantId)
|
||||
if (!quota) {
|
||||
// Create default quota
|
||||
quota = {
|
||||
currentBlobStorageBytes: 0,
|
||||
currentBlobCount: 0,
|
||||
currentRecords: 0,
|
||||
currentDataSizeBytes: 0
|
||||
}
|
||||
this.tenants.set(tenantId, quota)
|
||||
}
|
||||
|
||||
// Get or create identity
|
||||
const identityKey = `${tenantId}:${userId}`
|
||||
let identity = this.permissions.get(identityKey)
|
||||
if (!identity) {
|
||||
identity = {
|
||||
tenantId,
|
||||
userId,
|
||||
role: 'member',
|
||||
permissions: new Set(['read:*', 'write:*'])
|
||||
}
|
||||
this.permissions.set(identityKey, identity)
|
||||
}
|
||||
|
||||
const namespace = `tenants/${tenantId}/`
|
||||
|
||||
return new DefaultTenantContext(identity, quota, namespace)
|
||||
}
|
||||
|
||||
async updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (quota) {
|
||||
quota.currentBlobStorageBytes += bytesChange
|
||||
quota.currentBlobCount += countChange
|
||||
}
|
||||
}
|
||||
|
||||
async updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (quota) {
|
||||
quota.currentRecords += countChange
|
||||
quota.currentDataSizeBytes += bytesChange
|
||||
}
|
||||
}
|
||||
|
||||
async createTenant(tenantId: string, quotaOverrides?: Partial<TenantQuota>): Promise<void> {
|
||||
const quota: TenantQuota = {
|
||||
currentBlobStorageBytes: 0,
|
||||
currentBlobCount: 0,
|
||||
currentRecords: 0,
|
||||
currentDataSizeBytes: 0,
|
||||
...quotaOverrides
|
||||
}
|
||||
this.tenants.set(tenantId, quota)
|
||||
}
|
||||
|
||||
async updateQuota(tenantId: string, quotaUpdates: Partial<TenantQuota>): Promise<void> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (quota) {
|
||||
Object.assign(quota, quotaUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
async getUsage(tenantId: string): Promise<TenantQuota> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (!quota) {
|
||||
return {
|
||||
currentBlobStorageBytes: 0,
|
||||
currentBlobCount: 0,
|
||||
currentRecords: 0,
|
||||
currentDataSizeBytes: 0
|
||||
}
|
||||
}
|
||||
return { ...quota }
|
||||
}
|
||||
|
||||
// Admin methods for testing
|
||||
setUserRole(tenantId: string, userId: string, role: TenantIdentity['role']): void {
|
||||
const identityKey = `${tenantId}:${userId}`
|
||||
const identity = this.permissions.get(identityKey)
|
||||
if (identity) {
|
||||
identity.role = role
|
||||
}
|
||||
}
|
||||
|
||||
grantPermission(tenantId: string, userId: string, permission: string): void {
|
||||
const identityKey = `${tenantId}:${userId}`
|
||||
const identity = this.permissions.get(identityKey)
|
||||
if (identity) {
|
||||
identity.permissions.add(permission)
|
||||
}
|
||||
}
|
||||
export const createTenantContext = (
|
||||
identity: TenantIdentity,
|
||||
quota: TenantQuota,
|
||||
namespace?: string
|
||||
): TenantContext => {
|
||||
return new DefaultTenantContext(
|
||||
identity,
|
||||
quota,
|
||||
namespace || `tenant_${identity.tenantId}`
|
||||
)
|
||||
}
|
||||
|
||||
255
dbal/development/src/core/foundation/tenant-context.ts.backup
Normal file
255
dbal/development/src/core/foundation/tenant-context.ts.backup
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Multi-Tenant Context and Identity Management
|
||||
*
|
||||
* Provides tenant isolation, access control, and quota management
|
||||
* for both blob storage and structured data.
|
||||
*/
|
||||
|
||||
export interface TenantIdentity {
|
||||
tenantId: string
|
||||
userId: string
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer'
|
||||
permissions: Set<string>
|
||||
}
|
||||
|
||||
export interface TenantQuota {
|
||||
// Blob storage quotas
|
||||
maxBlobStorageBytes?: number
|
||||
maxBlobCount?: number
|
||||
maxBlobSizeBytes?: number
|
||||
|
||||
// Structured data quotas
|
||||
maxRecords?: number
|
||||
maxDataSizeBytes?: number
|
||||
maxListLength?: number
|
||||
|
||||
// Computed usage
|
||||
currentBlobStorageBytes: number
|
||||
currentBlobCount: number
|
||||
currentRecords: number
|
||||
currentDataSizeBytes: number
|
||||
}
|
||||
|
||||
export interface TenantContext {
|
||||
identity: TenantIdentity
|
||||
quota: TenantQuota
|
||||
namespace: string // For blob storage isolation
|
||||
|
||||
// Check if operation is allowed
|
||||
canRead(resource: string): boolean
|
||||
canWrite(resource: string): boolean
|
||||
canDelete(resource: string): boolean
|
||||
|
||||
// Check quota availability
|
||||
canUploadBlob(sizeBytes: number): boolean
|
||||
canCreateRecord(): boolean
|
||||
canAddToList(additionalItems: number): boolean
|
||||
}
|
||||
|
||||
export class DefaultTenantContext implements TenantContext {
|
||||
constructor(
|
||||
public readonly identity: TenantIdentity,
|
||||
public readonly quota: TenantQuota,
|
||||
public readonly namespace: string
|
||||
) {}
|
||||
|
||||
canRead(resource: string): boolean {
|
||||
// Owner and admin can read everything
|
||||
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check specific permissions
|
||||
return (
|
||||
this.identity.permissions.has('read:*') ||
|
||||
this.identity.permissions.has(`read:${resource}`)
|
||||
)
|
||||
}
|
||||
|
||||
canWrite(resource: string): boolean {
|
||||
// Only owner and admin can write
|
||||
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check specific permissions
|
||||
return (
|
||||
this.identity.permissions.has('write:*') ||
|
||||
this.identity.permissions.has(`write:${resource}`)
|
||||
)
|
||||
}
|
||||
|
||||
canDelete(resource: string): boolean {
|
||||
// Only owner and admin can delete
|
||||
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check specific permissions
|
||||
return (
|
||||
this.identity.permissions.has('delete:*') ||
|
||||
this.identity.permissions.has(`delete:${resource}`)
|
||||
)
|
||||
}
|
||||
|
||||
canUploadBlob(sizeBytes: number): boolean {
|
||||
const { quota } = this
|
||||
|
||||
// Check max blob size
|
||||
if (quota.maxBlobSizeBytes && sizeBytes > quota.maxBlobSizeBytes) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check total storage quota
|
||||
if (quota.maxBlobStorageBytes) {
|
||||
if (quota.currentBlobStorageBytes + sizeBytes > quota.maxBlobStorageBytes) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check blob count quota
|
||||
if (quota.maxBlobCount) {
|
||||
if (quota.currentBlobCount >= quota.maxBlobCount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
canCreateRecord(): boolean {
|
||||
const { quota } = this
|
||||
|
||||
if (quota.maxRecords) {
|
||||
return quota.currentRecords < quota.maxRecords
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
canAddToList(additionalItems: number): boolean {
|
||||
const { quota } = this
|
||||
|
||||
if (quota.maxListLength && additionalItems > quota.maxListLength) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export interface TenantManager {
|
||||
// Get tenant context for operations
|
||||
getTenantContext(tenantId: string, userId: string): Promise<TenantContext>
|
||||
|
||||
// Update quota usage
|
||||
updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void>
|
||||
updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void>
|
||||
|
||||
// Create/update tenant
|
||||
createTenant(tenantId: string, quota?: Partial<TenantQuota>): Promise<void>
|
||||
updateQuota(tenantId: string, quota: Partial<TenantQuota>): Promise<void>
|
||||
|
||||
// Get current usage
|
||||
getUsage(tenantId: string): Promise<TenantQuota>
|
||||
}
|
||||
|
||||
export class InMemoryTenantManager implements TenantManager {
|
||||
private tenants = new Map<string, TenantQuota>()
|
||||
private permissions = new Map<string, TenantIdentity>()
|
||||
|
||||
async getTenantContext(tenantId: string, userId: string): Promise<TenantContext> {
|
||||
let quota = this.tenants.get(tenantId)
|
||||
if (!quota) {
|
||||
// Create default quota
|
||||
quota = {
|
||||
currentBlobStorageBytes: 0,
|
||||
currentBlobCount: 0,
|
||||
currentRecords: 0,
|
||||
currentDataSizeBytes: 0
|
||||
}
|
||||
this.tenants.set(tenantId, quota)
|
||||
}
|
||||
|
||||
// Get or create identity
|
||||
const identityKey = `${tenantId}:${userId}`
|
||||
let identity = this.permissions.get(identityKey)
|
||||
if (!identity) {
|
||||
identity = {
|
||||
tenantId,
|
||||
userId,
|
||||
role: 'member',
|
||||
permissions: new Set(['read:*', 'write:*'])
|
||||
}
|
||||
this.permissions.set(identityKey, identity)
|
||||
}
|
||||
|
||||
const namespace = `tenants/${tenantId}/`
|
||||
|
||||
return new DefaultTenantContext(identity, quota, namespace)
|
||||
}
|
||||
|
||||
async updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (quota) {
|
||||
quota.currentBlobStorageBytes += bytesChange
|
||||
quota.currentBlobCount += countChange
|
||||
}
|
||||
}
|
||||
|
||||
async updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (quota) {
|
||||
quota.currentRecords += countChange
|
||||
quota.currentDataSizeBytes += bytesChange
|
||||
}
|
||||
}
|
||||
|
||||
async createTenant(tenantId: string, quotaOverrides?: Partial<TenantQuota>): Promise<void> {
|
||||
const quota: TenantQuota = {
|
||||
currentBlobStorageBytes: 0,
|
||||
currentBlobCount: 0,
|
||||
currentRecords: 0,
|
||||
currentDataSizeBytes: 0,
|
||||
...quotaOverrides
|
||||
}
|
||||
this.tenants.set(tenantId, quota)
|
||||
}
|
||||
|
||||
async updateQuota(tenantId: string, quotaUpdates: Partial<TenantQuota>): Promise<void> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (quota) {
|
||||
Object.assign(quota, quotaUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
async getUsage(tenantId: string): Promise<TenantQuota> {
|
||||
const quota = this.tenants.get(tenantId)
|
||||
if (!quota) {
|
||||
return {
|
||||
currentBlobStorageBytes: 0,
|
||||
currentBlobCount: 0,
|
||||
currentRecords: 0,
|
||||
currentDataSizeBytes: 0
|
||||
}
|
||||
}
|
||||
return { ...quota }
|
||||
}
|
||||
|
||||
// Admin methods for testing
|
||||
setUserRole(tenantId: string, userId: string, role: TenantIdentity['role']): void {
|
||||
const identityKey = `${tenantId}:${userId}`
|
||||
const identity = this.permissions.get(identityKey)
|
||||
if (identity) {
|
||||
identity.role = role
|
||||
}
|
||||
}
|
||||
|
||||
grantPermission(tenantId: string, userId: string, permission: string): void {
|
||||
const identityKey = `${tenantId}:${userId}`
|
||||
const identity = this.permissions.get(identityKey)
|
||||
if (identity) {
|
||||
identity.permissions.add(permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file permission-checks.ts
|
||||
* @description Permission checking utilities for tenant resources
|
||||
*/
|
||||
|
||||
import type { TenantIdentity } from './tenant-types'
|
||||
|
||||
/**
|
||||
* Check if tenant has read permission for a resource
|
||||
*/
|
||||
export const canRead = (identity: TenantIdentity, resource: string): boolean => {
|
||||
if (identity.role === 'owner' || identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
identity.permissions.has('read:*') ||
|
||||
identity.permissions.has(`read:${resource}`)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant has write permission for a resource
|
||||
*/
|
||||
export const canWrite = (identity: TenantIdentity, resource: string): boolean => {
|
||||
if (identity.role === 'owner' || identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
identity.permissions.has('write:*') ||
|
||||
identity.permissions.has(`write:${resource}`)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant has delete permission for a resource
|
||||
*/
|
||||
export const canDelete = (identity: TenantIdentity, resource: string): boolean => {
|
||||
if (identity.role === 'owner' || identity.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
identity.permissions.has('delete:*') ||
|
||||
identity.permissions.has(`delete:${resource}`)
|
||||
)
|
||||
}
|
||||
57
dbal/development/src/core/foundation/tenant/quota-checks.ts
Normal file
57
dbal/development/src/core/foundation/tenant/quota-checks.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @file quota-checks.ts
|
||||
* @description Quota checking utilities for tenant resources
|
||||
*/
|
||||
|
||||
import type { TenantQuota } from './tenant-types'
|
||||
|
||||
/**
|
||||
* Check if tenant can upload a blob of given size
|
||||
*/
|
||||
export const canUploadBlob = (quota: TenantQuota, sizeBytes: number): boolean => {
|
||||
// Check blob size limit
|
||||
if (quota.maxBlobSizeBytes && sizeBytes > quota.maxBlobSizeBytes) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check total storage limit
|
||||
if (quota.maxBlobStorageBytes) {
|
||||
const projectedTotal = quota.currentBlobStorageBytes + sizeBytes
|
||||
if (projectedTotal > quota.maxBlobStorageBytes) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check blob count limit
|
||||
if (quota.maxBlobCount && quota.currentBlobCount >= quota.maxBlobCount) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant can create a new record
|
||||
*/
|
||||
export const canCreateRecord = (quota: TenantQuota): boolean => {
|
||||
if (quota.maxRecords && quota.currentRecords >= quota.maxRecords) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tenant can add items to a list
|
||||
*/
|
||||
export const canAddToList = (quota: TenantQuota, additionalItems: number): boolean => {
|
||||
if (quota.maxListLength) {
|
||||
// Assuming currentRecords includes list items
|
||||
const projectedTotal = quota.currentRecords + additionalItems
|
||||
if (projectedTotal > quota.maxListLength) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
43
dbal/development/src/core/foundation/tenant/tenant-types.ts
Normal file
43
dbal/development/src/core/foundation/tenant/tenant-types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @file tenant-types.ts
|
||||
* @description Type definitions for tenant context and identity
|
||||
*/
|
||||
|
||||
export interface TenantIdentity {
|
||||
tenantId: string
|
||||
userId: string
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer'
|
||||
permissions: Set<string>
|
||||
}
|
||||
|
||||
export interface TenantQuota {
|
||||
// Blob storage quotas
|
||||
maxBlobStorageBytes?: number
|
||||
maxBlobCount?: number
|
||||
maxBlobSizeBytes?: number
|
||||
|
||||
// Structured data quotas
|
||||
maxRecords?: number
|
||||
maxDataSizeBytes?: number
|
||||
maxListLength?: number
|
||||
|
||||
// Computed usage
|
||||
currentBlobStorageBytes: number
|
||||
currentBlobCount: number
|
||||
currentRecords: number
|
||||
currentDataSizeBytes: number
|
||||
}
|
||||
|
||||
export interface TenantContext {
|
||||
identity: TenantIdentity
|
||||
quota: TenantQuota
|
||||
namespace: string
|
||||
|
||||
canRead(resource: string): boolean
|
||||
canWrite(resource: string): boolean
|
||||
canDelete(resource: string): boolean
|
||||
|
||||
canUploadBlob(sizeBytes: number): boolean
|
||||
canCreateRecord(): boolean
|
||||
canAddToList(additionalItems: number): boolean
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file delete-user.ts
|
||||
* @description DELETE handler for removing a user
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import {
|
||||
dbalDeleteUser,
|
||||
initializeDBAL,
|
||||
} from '@/lib/dbal/core/client/database-dbal.server'
|
||||
import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key'
|
||||
|
||||
interface RouteParams {
|
||||
params: {
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
const success = await dbalDeleteUser(params.userId)
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to delete user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file get-user.ts
|
||||
* @description GET handler for fetching a user by ID
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import {
|
||||
dbalGetUserById,
|
||||
initializeDBAL,
|
||||
} from '@/lib/dbal/core/client/database-dbal.server'
|
||||
import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key'
|
||||
|
||||
interface RouteParams {
|
||||
params: {
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
const user = await dbalGetUserById(params.userId)
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
} catch (error) {
|
||||
console.error('Error fetching user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @file patch-user.ts
|
||||
* @description PATCH handler for updating a user
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import {
|
||||
dbalUpdateUser,
|
||||
initializeDBAL,
|
||||
} from '@/lib/dbal/core/client/database-dbal.server'
|
||||
import { hashPassword } from '@/lib/db/hash-password'
|
||||
import { setCredential } from '@/lib/db/credentials/set-credential'
|
||||
import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key'
|
||||
import { normalizeRole, readJson } from '../utils/request-helpers'
|
||||
|
||||
interface RouteParams {
|
||||
params: {
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
|
||||
const body = await readJson<{
|
||||
username?: string
|
||||
email?: string
|
||||
role?: string
|
||||
password?: string
|
||||
profilePicture?: string
|
||||
bio?: string
|
||||
tenantId?: string
|
||||
isInstanceOwner?: boolean
|
||||
}>(request)
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { password, role, ...updateFields } = body
|
||||
const normalizedRole = normalizeRole(role)
|
||||
|
||||
const updatedUser = await dbalUpdateUser(params.userId, {
|
||||
...updateFields,
|
||||
...(normalizedRole && { role: normalizedRole }),
|
||||
})
|
||||
|
||||
if (password) {
|
||||
const hashedPassword = await hashPassword(password)
|
||||
await setCredential({
|
||||
username: updatedUser.username,
|
||||
passwordHash: hashedPassword,
|
||||
userId: updatedUser.id,
|
||||
firstLogin: false,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ user: updatedUser })
|
||||
} catch (error) {
|
||||
console.error('Error updating user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to update user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import {
|
||||
dbalDeleteUser,
|
||||
dbalGetUserById,
|
||||
dbalUpdateUser,
|
||||
initializeDBAL,
|
||||
} from '@/lib/dbal/core/client/database-dbal.server'
|
||||
import { hashPassword } from '@/lib/db/hash-password'
|
||||
import { setCredential } from '@/lib/db/credentials/set-credential'
|
||||
import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key'
|
||||
import type { UserRole } from '@/lib/level-types'
|
||||
/**
|
||||
* @file route.ts
|
||||
* @description User API route handlers aggregated from handler modules
|
||||
*/
|
||||
|
||||
function normalizeRole(role?: string): UserRole | undefined {
|
||||
if (!role) return undefined
|
||||
if (role === 'public') return 'user'
|
||||
return role as UserRole
|
||||
}
|
||||
|
||||
async function readJson<T>(request: NextRequest): Promise<T | null> {
|
||||
try {
|
||||
return (await request.json()) as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
params: {
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
const user = await dbalGetUserById(params.userId)
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
} catch (error) {
|
||||
console.error('Error fetching user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
|
||||
const body = await readJson<{
|
||||
username?: string
|
||||
email?: string
|
||||
role?: string
|
||||
password?: string
|
||||
profilePicture?: string
|
||||
bio?: string
|
||||
tenantId?: string
|
||||
isInstanceOwner?: boolean
|
||||
}>(request)
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (body.username) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Username updates are not supported' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const existingUser = await dbalGetUserById(params.userId)
|
||||
if (!existingUser) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const updates = {
|
||||
email: typeof body.email === 'string' ? body.email.trim() : undefined,
|
||||
role: normalizeRole(body.role),
|
||||
profilePicture: body.profilePicture,
|
||||
bio: body.bio,
|
||||
tenantId: body.tenantId,
|
||||
isInstanceOwner: body.isInstanceOwner,
|
||||
}
|
||||
|
||||
const user = await dbalUpdateUser(params.userId, updates)
|
||||
|
||||
if (typeof body.password === 'string' && body.password.length > 0) {
|
||||
const passwordHash = await hashPassword(body.password)
|
||||
await setCredential(existingUser.username, passwordHash)
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
} catch (error) {
|
||||
console.error('Error updating user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to update user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
|
||||
const existingUser = await dbalGetUserById(params.userId)
|
||||
if (!existingUser) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
await dbalDeleteUser(params.userId)
|
||||
await setCredential(existingUser.username, '')
|
||||
|
||||
return NextResponse.json({ deleted: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to delete user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
export { GET } from './handlers/get-user'
|
||||
export { PATCH } from './handlers/patch-user'
|
||||
export { DELETE } from './handlers/delete-user'
|
||||
|
||||
151
frontends/nextjs/src/app/api/users/[userId]/route.ts.backup
Normal file
151
frontends/nextjs/src/app/api/users/[userId]/route.ts.backup
Normal file
@@ -0,0 +1,151 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import {
|
||||
dbalDeleteUser,
|
||||
dbalGetUserById,
|
||||
dbalUpdateUser,
|
||||
initializeDBAL,
|
||||
} from '@/lib/dbal/core/client/database-dbal.server'
|
||||
import { hashPassword } from '@/lib/db/hash-password'
|
||||
import { setCredential } from '@/lib/db/credentials/set-credential'
|
||||
import { requireDBALApiKey } from '@/lib/api/require-dbal-api-key'
|
||||
import type { UserRole } from '@/lib/level-types'
|
||||
|
||||
function normalizeRole(role?: string): UserRole | undefined {
|
||||
if (!role) return undefined
|
||||
if (role === 'public') return 'user'
|
||||
return role as UserRole
|
||||
}
|
||||
|
||||
async function readJson<T>(request: NextRequest): Promise<T | null> {
|
||||
try {
|
||||
return (await request.json()) as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
params: {
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
const user = await dbalGetUserById(params.userId)
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
} catch (error) {
|
||||
console.error('Error fetching user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
|
||||
const body = await readJson<{
|
||||
username?: string
|
||||
email?: string
|
||||
role?: string
|
||||
password?: string
|
||||
profilePicture?: string
|
||||
bio?: string
|
||||
tenantId?: string
|
||||
isInstanceOwner?: boolean
|
||||
}>(request)
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (body.username) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Username updates are not supported' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const existingUser = await dbalGetUserById(params.userId)
|
||||
if (!existingUser) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const updates = {
|
||||
email: typeof body.email === 'string' ? body.email.trim() : undefined,
|
||||
role: normalizeRole(body.role),
|
||||
profilePicture: body.profilePicture,
|
||||
bio: body.bio,
|
||||
tenantId: body.tenantId,
|
||||
isInstanceOwner: body.isInstanceOwner,
|
||||
}
|
||||
|
||||
const user = await dbalUpdateUser(params.userId, updates)
|
||||
|
||||
if (typeof body.password === 'string' && body.password.length > 0) {
|
||||
const passwordHash = await hashPassword(body.password)
|
||||
await setCredential(existingUser.username, passwordHash)
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
} catch (error) {
|
||||
console.error('Error updating user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to update user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
const unauthorized = requireDBALApiKey(request)
|
||||
if (unauthorized) {
|
||||
return unauthorized
|
||||
}
|
||||
try {
|
||||
await initializeDBAL()
|
||||
|
||||
const existingUser = await dbalGetUserById(params.userId)
|
||||
if (!existingUser) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
await dbalDeleteUser(params.userId)
|
||||
await setCredential(existingUser.username, '')
|
||||
|
||||
return NextResponse.json({ deleted: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting user via DBAL:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to delete user',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @file request-helpers.ts
|
||||
* @description Helper functions for API request processing
|
||||
*/
|
||||
|
||||
import type { NextRequest } from 'next/server'
|
||||
import type { UserRole } from '@/lib/level-types'
|
||||
|
||||
/**
|
||||
* Normalize role string to UserRole type
|
||||
*/
|
||||
export function normalizeRole(role?: string): UserRole | undefined {
|
||||
if (!role) return undefined
|
||||
if (role === 'public') return 'user'
|
||||
return role as UserRole
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse JSON from request body
|
||||
*/
|
||||
export async function readJson<T>(request: NextRequest): Promise<T | null> {
|
||||
try {
|
||||
return (await request.json()) as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,11 @@
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Container,
|
||||
Divider,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Container, Stack, Typography } from '@mui/material'
|
||||
|
||||
import { PERMISSION_LEVELS, type PermissionLevel } from './levels-data'
|
||||
|
||||
const highlightColor = (level: PermissionLevel) => {
|
||||
if (level.id === 6) return 'warning.main'
|
||||
if (level.id === 5) return 'primary.main'
|
||||
return 'divider'
|
||||
}
|
||||
import { LevelDetails } from './components/LevelDetails'
|
||||
import { LevelsGrid } from './components/LevelsGrid'
|
||||
import { PERMISSION_LEVELS } from './levels-data'
|
||||
|
||||
export default function LevelsClient() {
|
||||
const [selectedLevelId, setSelectedLevelId] = useState(PERMISSION_LEVELS[0].id)
|
||||
@@ -70,94 +54,19 @@ export default function LevelsClient() {
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{PERMISSION_LEVELS.map((level) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={level.id} component="div">
|
||||
<Paper
|
||||
onClick={() => handleSelect(level.id)}
|
||||
sx={{
|
||||
border: (theme) => `2px solid ${selectedLevel.id === level.id ? theme.palette.primary.main : theme.palette.divider}`,
|
||||
p: 3,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
},
|
||||
}}
|
||||
elevation={selectedLevel.id === level.id ? 6 : 1}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', top: 16, right: 16 }}>
|
||||
<Chip label={level.badge} />
|
||||
</Box>
|
||||
<Typography variant="h6">Level {level.id} · {level.title}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{level.tagline}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
{level.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{level.capabilities.slice(0, 3).map((capability) => (
|
||||
<Chip key={capability} label={capability} size="small" variant="outlined" />
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<LevelsGrid
|
||||
levels={PERMISSION_LEVELS}
|
||||
onSelect={handleSelect}
|
||||
selectedLevelId={selectedLevelId}
|
||||
/>
|
||||
|
||||
<Paper sx={{ p: 4, border: (theme) => `1px dashed ${theme.palette.divider}`, bgcolor: 'background.paper' }}>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Typography variant="h5">Selected level details</Typography>
|
||||
<Chip label={selectedLevel.badge} size="small" color="secondary" />
|
||||
</Stack>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{selectedLevel.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{selectedLevel.capabilities.map((capability) => (
|
||||
<Chip
|
||||
key={capability}
|
||||
label={capability}
|
||||
size="small"
|
||||
sx={{ borderColor: highlightColor(selectedLevel) }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(selectedLevel.capabilities.length / maxCapabilityCount) * 100}
|
||||
sx={{ height: 10, borderRadius: 2 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{selectedLevel.capabilities.length} of {maxCapabilityCount} capability tiers unlocked
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Next move
|
||||
</Typography>
|
||||
{nextLevel ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Promote into <strong>{nextLevel.title}</strong> to unlock {nextLevel.capabilities.length} controls.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Super God reigns supreme. You already own every privilege.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant="contained" onClick={handlePromote}>
|
||||
{nextLevel ? `Promote to ${nextLevel.title}` : 'Hold the crown'}
|
||||
</Button>
|
||||
</Box>
|
||||
{note && <Alert severity="info">{note}</Alert>}
|
||||
</Stack>
|
||||
</Paper>
|
||||
<LevelDetails
|
||||
selectedLevel={selectedLevel}
|
||||
nextLevel={nextLevel}
|
||||
maxCapabilityCount={maxCapabilityCount}
|
||||
note={note}
|
||||
onPromote={handlePromote}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
|
||||
67
frontends/nextjs/src/app/levels/components/LevelDetails.tsx
Normal file
67
frontends/nextjs/src/app/levels/components/LevelDetails.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Alert, Box, Button, Chip, Divider, LinearProgress, Paper, Stack, Typography } from '@mui/material'
|
||||
|
||||
import type { PermissionLevel } from '../levels-data'
|
||||
import { highlightColor } from '../utils/highlightColor'
|
||||
|
||||
type LevelDetailsProps = {
|
||||
selectedLevel: PermissionLevel
|
||||
nextLevel: PermissionLevel | null
|
||||
maxCapabilityCount: number
|
||||
note: string
|
||||
onPromote: () => void
|
||||
}
|
||||
|
||||
export const LevelDetails = ({ selectedLevel, nextLevel, maxCapabilityCount, note, onPromote }: LevelDetailsProps) => (
|
||||
<Paper sx={{ p: 4, border: (theme) => `1px dashed ${theme.palette.divider}`, bgcolor: 'background.paper' }}>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Typography variant="h5">Selected level details</Typography>
|
||||
<Chip label={selectedLevel.badge} size="small" color="secondary" />
|
||||
</Stack>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{selectedLevel.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{selectedLevel.capabilities.map((capability) => (
|
||||
<Chip
|
||||
key={capability}
|
||||
label={capability}
|
||||
size="small"
|
||||
sx={{ borderColor: highlightColor(selectedLevel) }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(selectedLevel.capabilities.length / maxCapabilityCount) * 100}
|
||||
sx={{ height: 10, borderRadius: 2 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{selectedLevel.capabilities.length} of {maxCapabilityCount} capability tiers unlocked
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Next move
|
||||
</Typography>
|
||||
{nextLevel ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Promote into <strong>{nextLevel.title}</strong> to unlock {nextLevel.capabilities.length} controls.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Super God reigns supreme. You already own every privilege.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant="contained" onClick={onPromote}>
|
||||
{nextLevel ? `Promote to ${nextLevel.title}` : 'Hold the crown'}
|
||||
</Button>
|
||||
</Box>
|
||||
{note && <Alert severity="info">{note}</Alert>}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
47
frontends/nextjs/src/app/levels/components/LevelsGrid.tsx
Normal file
47
frontends/nextjs/src/app/levels/components/LevelsGrid.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Box, Chip, Grid, Paper, Stack, Typography } from '@mui/material'
|
||||
|
||||
import type { PermissionLevel } from '../levels-data'
|
||||
|
||||
type LevelsGridProps = {
|
||||
levels: PermissionLevel[]
|
||||
selectedLevelId: number
|
||||
onSelect: (levelId: number) => void
|
||||
}
|
||||
|
||||
export const LevelsGrid = ({ levels, selectedLevelId, onSelect }: LevelsGridProps) => (
|
||||
<Grid container spacing={3}>
|
||||
{levels.map((level) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={level.id} component="div">
|
||||
<Paper
|
||||
onClick={() => onSelect(level.id)}
|
||||
sx={{
|
||||
border: (theme) => `2px solid ${selectedLevelId === level.id ? theme.palette.primary.main : theme.palette.divider}`,
|
||||
p: 3,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
},
|
||||
}}
|
||||
elevation={selectedLevelId === level.id ? 6 : 1}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', top: 16, right: 16 }}>
|
||||
<Chip label={level.badge} />
|
||||
</Box>
|
||||
<Typography variant="h6">Level {level.id} · {level.title}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{level.tagline}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
{level.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{level.capabilities.slice(0, 3).map((capability) => (
|
||||
<Chip key={capability} label={capability} size="small" variant="outlined" />
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
7
frontends/nextjs/src/app/levels/utils/highlightColor.ts
Normal file
7
frontends/nextjs/src/app/levels/utils/highlightColor.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { PermissionLevel } from '../levels-data'
|
||||
|
||||
export const highlightColor = (level: PermissionLevel) => {
|
||||
if (level.id === 6) return 'warning.main'
|
||||
if (level.id === 5) return 'primary.main'
|
||||
return 'divider'
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Level4Header } from '../../level4/Level4Header'
|
||||
import { Level4Tabs } from '../../level4/Level4Tabs'
|
||||
import { Level4Summary } from '../../level4/Level4Summary'
|
||||
import { NerdModeIDE } from '../../misc/NerdModeIDE'
|
||||
import { Database } from '@/lib/database'
|
||||
import { seedDatabase } from '@/lib/seed-data'
|
||||
import type { User as UserType, AppConfiguration } from '@/lib/level-types'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import type { User as UserType } from '@/lib/level-types'
|
||||
import { useLevel4AppState } from './hooks/useLevel4AppState'
|
||||
|
||||
interface Level4Props {
|
||||
user: UserType
|
||||
@@ -19,94 +15,29 @@ interface Level4Props {
|
||||
}
|
||||
|
||||
export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
const [appConfig, setAppConfig] = useState<AppConfiguration | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [nerdMode, setNerdMode] = useKV<boolean>('level4-nerd-mode', false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
await seedDatabase()
|
||||
|
||||
const config = await Database.getAppConfig()
|
||||
if (config) {
|
||||
setAppConfig(config)
|
||||
} else {
|
||||
const defaultConfig: AppConfiguration = {
|
||||
id: 'app_001',
|
||||
name: 'MetaBuilder App',
|
||||
schemas: [],
|
||||
workflows: [],
|
||||
luaScripts: [],
|
||||
pages: [],
|
||||
theme: {
|
||||
colors: {},
|
||||
fonts: {},
|
||||
},
|
||||
}
|
||||
await Database.setAppConfig(defaultConfig)
|
||||
setAppConfig(defaultConfig)
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
loadConfig()
|
||||
}, [])
|
||||
const {
|
||||
appConfig,
|
||||
handleExportConfig,
|
||||
handleImportConfig,
|
||||
handleLuaScriptsChange,
|
||||
handleSchemasChange,
|
||||
handleWorkflowsChange,
|
||||
isLoading,
|
||||
nerdMode,
|
||||
toggleNerdMode,
|
||||
} = useLevel4AppState()
|
||||
|
||||
if (isLoading || !appConfig) return null
|
||||
|
||||
const updateAppConfig = async (updates: Partial<AppConfiguration>) => {
|
||||
const newConfig = { ...appConfig, ...updates }
|
||||
setAppConfig(newConfig)
|
||||
await Database.setAppConfig(newConfig)
|
||||
}
|
||||
|
||||
const handleExportConfig = async () => {
|
||||
const dataStr = await Database.exportDatabase()
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(dataBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'database-export.json'
|
||||
link.click()
|
||||
toast.success('Database exported')
|
||||
}
|
||||
|
||||
const handleImportConfig = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'application/json'
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const text = await file.text()
|
||||
try {
|
||||
await Database.importDatabase(text)
|
||||
const newConfig = await Database.getAppConfig()
|
||||
if (newConfig) {
|
||||
setAppConfig(newConfig)
|
||||
}
|
||||
toast.success('Database imported successfully')
|
||||
} catch (error) {
|
||||
toast.error('Invalid database file')
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const handleToggleNerdMode = () => {
|
||||
setNerdMode(!nerdMode)
|
||||
toast.info(nerdMode ? 'Nerd Mode disabled' : 'Nerd Mode enabled')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas">
|
||||
<Level4Header
|
||||
username={user.username}
|
||||
nerdMode={nerdMode || false}
|
||||
nerdMode={nerdMode}
|
||||
onNavigate={onNavigate}
|
||||
onPreview={onPreview}
|
||||
onLogout={onLogout}
|
||||
onToggleNerdMode={handleToggleNerdMode}
|
||||
onToggleNerdMode={toggleNerdMode}
|
||||
onExportConfig={handleExportConfig}
|
||||
onImportConfig={handleImportConfig}
|
||||
/>
|
||||
@@ -115,7 +46,7 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Application Builder</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{nerdMode
|
||||
{nerdMode
|
||||
? "Design your application declaratively. Define schemas, create workflows, and write Lua scripts."
|
||||
: "Build your application visually. Configure pages, users, and data models with simple forms."
|
||||
}
|
||||
@@ -124,25 +55,13 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
|
||||
<Level4Tabs
|
||||
appConfig={appConfig}
|
||||
nerdMode={nerdMode || false}
|
||||
onSchemasChange={async (schemas) => {
|
||||
const newConfig = { ...appConfig, schemas }
|
||||
setAppConfig(newConfig)
|
||||
await Database.setAppConfig(newConfig)
|
||||
}}
|
||||
onWorkflowsChange={async (workflows) => {
|
||||
const newConfig = { ...appConfig, workflows }
|
||||
setAppConfig(newConfig)
|
||||
await Database.setAppConfig(newConfig)
|
||||
}}
|
||||
onLuaScriptsChange={async (scripts) => {
|
||||
const newConfig = { ...appConfig, luaScripts: scripts }
|
||||
setAppConfig(newConfig)
|
||||
await Database.setAppConfig(newConfig)
|
||||
}}
|
||||
nerdMode={nerdMode}
|
||||
onSchemasChange={handleSchemasChange}
|
||||
onWorkflowsChange={handleWorkflowsChange}
|
||||
onLuaScriptsChange={handleLuaScriptsChange}
|
||||
/>
|
||||
|
||||
<Level4Summary appConfig={appConfig} nerdMode={nerdMode || false} />
|
||||
<Level4Summary appConfig={appConfig} nerdMode={nerdMode} />
|
||||
|
||||
{nerdMode && (
|
||||
<div className="fixed bottom-4 right-4 w-[calc(100%-2rem)] max-w-[1400px] h-[600px] z-50 shadow-2xl">
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
|
||||
import { Database } from '@/lib/database'
|
||||
import type { AppConfiguration } from '@/lib/level-types'
|
||||
import { seedDatabase } from '@/lib/seed-data'
|
||||
|
||||
type ConfigUpdater = (config: AppConfiguration) => AppConfiguration
|
||||
|
||||
const createDefaultConfig = (): AppConfiguration => ({
|
||||
id: 'app_001',
|
||||
name: 'MetaBuilder App',
|
||||
schemas: [],
|
||||
workflows: [],
|
||||
luaScripts: [],
|
||||
pages: [],
|
||||
theme: {
|
||||
colors: {},
|
||||
fonts: {},
|
||||
},
|
||||
})
|
||||
|
||||
const persistConfig = async (config: AppConfiguration, setConfig: (value: AppConfiguration) => void) => {
|
||||
setConfig(config)
|
||||
await Database.setAppConfig(config)
|
||||
}
|
||||
|
||||
export const useLevel4AppState = () => {
|
||||
const [appConfig, setAppConfig] = useState<AppConfiguration | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [nerdMode, setNerdMode] = useKV<boolean>('level4-nerd-mode', false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
await seedDatabase()
|
||||
|
||||
const config = await Database.getAppConfig()
|
||||
if (config) {
|
||||
setAppConfig(config)
|
||||
} else {
|
||||
const defaultConfig = createDefaultConfig()
|
||||
await persistConfig(defaultConfig, setAppConfig)
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
void loadConfig()
|
||||
}, [])
|
||||
|
||||
const updateConfig = useCallback(
|
||||
async (updater: ConfigUpdater) => {
|
||||
if (!appConfig) return
|
||||
|
||||
const updatedConfig = updater(appConfig)
|
||||
await persistConfig(updatedConfig, setAppConfig)
|
||||
},
|
||||
[appConfig]
|
||||
)
|
||||
|
||||
const handleExportConfig = useCallback(async () => {
|
||||
const dataStr = await Database.exportDatabase()
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(dataBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'database-export.json'
|
||||
link.click()
|
||||
toast.success('Database exported')
|
||||
}, [])
|
||||
|
||||
const handleImportConfig = useCallback(() => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'application/json'
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const text = await file.text()
|
||||
try {
|
||||
await Database.importDatabase(text)
|
||||
const newConfig = await Database.getAppConfig()
|
||||
if (newConfig) {
|
||||
await persistConfig(newConfig, setAppConfig)
|
||||
}
|
||||
toast.success('Database imported successfully')
|
||||
} catch (error) {
|
||||
toast.error('Invalid database file')
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}, [])
|
||||
|
||||
const toggleNerdMode = useCallback(() => {
|
||||
const nextValue = !nerdMode
|
||||
setNerdMode(nextValue)
|
||||
toast.info(nextValue ? 'Nerd Mode enabled' : 'Nerd Mode disabled')
|
||||
}, [nerdMode, setNerdMode])
|
||||
|
||||
const handleSchemasChange = useCallback(
|
||||
async (schemas: AppConfiguration['schemas']) => updateConfig((config) => ({ ...config, schemas })),
|
||||
[updateConfig]
|
||||
)
|
||||
|
||||
const handleWorkflowsChange = useCallback(
|
||||
async (workflows: AppConfiguration['workflows']) => updateConfig((config) => ({ ...config, workflows })),
|
||||
[updateConfig]
|
||||
)
|
||||
|
||||
const handleLuaScriptsChange = useCallback(
|
||||
async (luaScripts: AppConfiguration['luaScripts']) => updateConfig((config) => ({ ...config, luaScripts })),
|
||||
[updateConfig]
|
||||
)
|
||||
|
||||
return {
|
||||
appConfig,
|
||||
isLoading,
|
||||
nerdMode: nerdMode || false,
|
||||
handleExportConfig,
|
||||
handleImportConfig,
|
||||
toggleNerdMode,
|
||||
handleSchemasChange,
|
||||
handleWorkflowsChange,
|
||||
handleLuaScriptsChange,
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
import {
|
||||
Select as MuiSelect,
|
||||
SelectProps as MuiSelectProps,
|
||||
MenuItem,
|
||||
MenuItemProps,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormHelperText,
|
||||
Box,
|
||||
} from '@mui/material'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import { FormControl, FormHelperText, InputLabel, Select as MuiSelect, SelectProps as MuiSelectProps } from '@mui/material'
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
import { SelectContent } from './SelectContent'
|
||||
import { SelectGroup } from './SelectGroup'
|
||||
import { SelectItem } from './SelectItem'
|
||||
import type { SelectItemProps } from './SelectItem'
|
||||
import { SelectLabel } from './SelectLabel'
|
||||
import { SelectSeparator } from './SelectSeparator'
|
||||
import { SelectTrigger } from './SelectTrigger'
|
||||
import { SelectValue } from './SelectValue'
|
||||
|
||||
// Select wrapper with FormControl
|
||||
export interface SelectProps extends Omit<MuiSelectProps<string>, 'onChange'> {
|
||||
onValueChange?: (value: string) => void
|
||||
helperText?: ReactNode
|
||||
@@ -42,119 +41,5 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
|
||||
)
|
||||
Select.displayName = 'Select'
|
||||
|
||||
// SelectTrigger (shadcn compat - wraps select display)
|
||||
interface SelectTriggerProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectTrigger = forwardRef<HTMLDivElement, SelectTriggerProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
borderColor: 'text.secondary',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<KeyboardArrowDownIcon fontSize="small" sx={{ ml: 1, color: 'text.secondary' }} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SelectTrigger.displayName = 'SelectTrigger'
|
||||
|
||||
// SelectValue (placeholder display)
|
||||
interface SelectValueProps {
|
||||
placeholder?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(
|
||||
({ placeholder, children, ...props }, ref) => {
|
||||
return (
|
||||
<Box component="span" ref={ref} sx={{ color: children ? 'text.primary' : 'text.secondary' }} {...props}>
|
||||
{children || placeholder}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SelectValue.displayName = 'SelectValue'
|
||||
|
||||
// SelectContent (dropdown container - just passes children in MUI)
|
||||
const SelectContent = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, ...props }, ref) => {
|
||||
return <>{children}</>
|
||||
}
|
||||
)
|
||||
SelectContent.displayName = 'SelectContent'
|
||||
|
||||
// SelectItem
|
||||
export interface SelectItemProps extends MenuItemProps {
|
||||
textValue?: string
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLLIElement, SelectItemProps>(
|
||||
({ value, children, textValue, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem ref={ref} value={value} {...props}>
|
||||
{children}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
SelectItem.displayName = 'SelectItem'
|
||||
|
||||
// SelectGroup
|
||||
const SelectGroup = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, ...props }, ref) => {
|
||||
return <Box ref={ref} {...props}>{children}</Box>
|
||||
}
|
||||
)
|
||||
SelectGroup.displayName = 'SelectGroup'
|
||||
|
||||
// SelectLabel
|
||||
const SelectLabel = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{ px: 2, py: 1, fontSize: '0.75rem', fontWeight: 600, color: 'text.secondary' }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SelectLabel.displayName = 'SelectLabel'
|
||||
|
||||
// SelectSeparator
|
||||
const SelectSeparator = forwardRef<HTMLHRElement>((props, ref) => {
|
||||
return <Box ref={ref} component="hr" sx={{ my: 0.5, borderColor: 'divider' }} {...props} />
|
||||
})
|
||||
SelectSeparator.displayName = 'SelectSeparator'
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
}
|
||||
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue }
|
||||
export type { SelectItemProps }
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
interface SelectContentProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectContent = forwardRef<HTMLDivElement, SelectContentProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
SelectContent.displayName = 'SelectContent'
|
||||
|
||||
export { SelectContent }
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
interface SelectGroupProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectGroup = forwardRef<HTMLDivElement, SelectGroupProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
SelectGroup.displayName = 'SelectGroup'
|
||||
|
||||
export { SelectGroup }
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { MenuItem, MenuItemProps } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export interface SelectItemProps extends MenuItemProps {
|
||||
textValue?: string
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLLIElement, SelectItemProps>(({ value, children, ...props }, ref) => {
|
||||
return (
|
||||
<MenuItem ref={ref} value={value} {...props}>
|
||||
{children}
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
|
||||
SelectItem.displayName = 'SelectItem'
|
||||
|
||||
export { SelectItem }
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
interface SelectLabelProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectLabel = forwardRef<HTMLDivElement, SelectLabelProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box ref={ref} sx={{ px: 2, py: 1, fontSize: '0.75rem', fontWeight: 600, color: 'text.secondary' }} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
SelectLabel.displayName = 'SelectLabel'
|
||||
|
||||
export { SelectLabel }
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
const SelectSeparator = forwardRef<HTMLHRElement>((props, ref) => {
|
||||
return <Box ref={ref} component="hr" sx={{ my: 0.5, borderColor: 'divider' }} {...props} />
|
||||
})
|
||||
|
||||
SelectSeparator.displayName = 'SelectSeparator'
|
||||
|
||||
export { SelectSeparator }
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import { Box } from '@mui/material'
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
interface SelectTriggerProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectTrigger = forwardRef<HTMLDivElement, SelectTriggerProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
borderColor: 'text.secondary',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<KeyboardArrowDownIcon fontSize="small" sx={{ ml: 1, color: 'text.secondary' }} />
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
SelectTrigger.displayName = 'SelectTrigger'
|
||||
|
||||
export { SelectTrigger }
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
|
||||
interface SelectValueProps {
|
||||
placeholder?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(({ placeholder, children, ...props }, ref) => {
|
||||
return (
|
||||
<Box component="span" ref={ref} sx={{ color: children ? 'text.primary' : 'text.secondary' }} {...props}>
|
||||
{children || placeholder}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
SelectValue.displayName = 'SelectValue'
|
||||
|
||||
export { SelectValue }
|
||||
@@ -1,18 +1,14 @@
|
||||
import type { User } from '@/lib/level-types'
|
||||
/**
|
||||
* @file auth-store.ts
|
||||
* @description Authentication state management store
|
||||
*/
|
||||
|
||||
import { fetchSession } from '@/lib/auth/api/fetch-session'
|
||||
import { login as loginRequest } from '@/lib/auth/api/login'
|
||||
import { logout as logoutRequest } from '@/lib/auth/api/logout'
|
||||
import { register as registerRequest } from '@/lib/auth/api/register'
|
||||
import type { AuthState, AuthUser } from './auth-types'
|
||||
|
||||
const roleLevels: Record<string, number> = {
|
||||
public: 1,
|
||||
user: 2,
|
||||
moderator: 3,
|
||||
admin: 4,
|
||||
god: 5,
|
||||
supergod: 6,
|
||||
}
|
||||
import type { AuthState } from './auth-types'
|
||||
import { mapUserToAuthUser } from './utils/map-user'
|
||||
|
||||
export class AuthStore {
|
||||
private state: AuthState = {
|
||||
@@ -35,6 +31,11 @@ export class AuthStore {
|
||||
}
|
||||
}
|
||||
|
||||
private setState(newState: AuthState): void {
|
||||
this.state = newState
|
||||
this.listeners.forEach(listener => listener())
|
||||
}
|
||||
|
||||
async ensureSessionChecked(): Promise<void> {
|
||||
if (!this.sessionCheckPromise) {
|
||||
this.sessionCheckPromise = this.refresh().finally(() => {
|
||||
@@ -53,7 +54,7 @@ export class AuthStore {
|
||||
try {
|
||||
const user = await loginRequest(identifier, password)
|
||||
this.setState({
|
||||
user: this.mapUserToAuthUser(user),
|
||||
user: mapUserToAuthUser(user),
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
})
|
||||
@@ -75,7 +76,7 @@ export class AuthStore {
|
||||
try {
|
||||
const user = await registerRequest(username, email, password)
|
||||
this.setState({
|
||||
user: this.mapUserToAuthUser(user),
|
||||
user: mapUserToAuthUser(user),
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
})
|
||||
@@ -89,24 +90,14 @@ export class AuthStore {
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await logoutRequest()
|
||||
} finally {
|
||||
this.setState({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: false,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,41 +108,28 @@ export class AuthStore {
|
||||
})
|
||||
|
||||
try {
|
||||
const sessionUser = await fetchSession()
|
||||
this.setState({
|
||||
user: sessionUser ? this.mapUserToAuthUser(sessionUser) : null,
|
||||
isAuthenticated: Boolean(sessionUser),
|
||||
isLoading: false,
|
||||
})
|
||||
const user = await fetchSession()
|
||||
if (user) {
|
||||
this.setState({
|
||||
user: mapUserToAuthUser(user),
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh auth session:', error)
|
||||
this.setState({
|
||||
...this.state,
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private mapUserToAuthUser(user: User): AuthUser {
|
||||
const level = roleLevels[user.role]
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
name: user.username,
|
||||
role: user.role,
|
||||
level,
|
||||
tenantId: user.tenantId,
|
||||
profilePicture: user.profilePicture,
|
||||
bio: user.bio,
|
||||
isInstanceOwner: user.isInstanceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
private setState(next: AuthState): void {
|
||||
this.state = next
|
||||
this.listeners.forEach((listener) => listener())
|
||||
}
|
||||
}
|
||||
|
||||
export const authStore = new AuthStore()
|
||||
|
||||
157
frontends/nextjs/src/hooks/auth/auth-store.ts.backup
Normal file
157
frontends/nextjs/src/hooks/auth/auth-store.ts.backup
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { User } from '@/lib/level-types'
|
||||
import { fetchSession } from '@/lib/auth/api/fetch-session'
|
||||
import { login as loginRequest } from '@/lib/auth/api/login'
|
||||
import { logout as logoutRequest } from '@/lib/auth/api/logout'
|
||||
import { register as registerRequest } from '@/lib/auth/api/register'
|
||||
import type { AuthState, AuthUser } from './auth-types'
|
||||
|
||||
const roleLevels: Record<string, number> = {
|
||||
public: 1,
|
||||
user: 2,
|
||||
moderator: 3,
|
||||
admin: 4,
|
||||
god: 5,
|
||||
supergod: 6,
|
||||
}
|
||||
|
||||
export class AuthStore {
|
||||
private state: AuthState = {
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
private listeners = new Set<() => void>()
|
||||
private sessionCheckPromise: Promise<void> | null = null
|
||||
|
||||
getState(): AuthState {
|
||||
return this.state
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener)
|
||||
return () => {
|
||||
this.listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
async ensureSessionChecked(): Promise<void> {
|
||||
if (!this.sessionCheckPromise) {
|
||||
this.sessionCheckPromise = this.refresh().finally(() => {
|
||||
this.sessionCheckPromise = null
|
||||
})
|
||||
}
|
||||
return this.sessionCheckPromise
|
||||
}
|
||||
|
||||
async login(identifier: string, password: string): Promise<void> {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
const user = await loginRequest(identifier, password)
|
||||
this.setState({
|
||||
user: this.mapUserToAuthUser(user),
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: false,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async register(username: string, email: string, password: string): Promise<void> {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
const user = await registerRequest(username, email, password)
|
||||
this.setState({
|
||||
user: this.mapUserToAuthUser(user),
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: false,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await logoutRequest()
|
||||
this.setState({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: false,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
const sessionUser = await fetchSession()
|
||||
this.setState({
|
||||
user: sessionUser ? this.mapUserToAuthUser(sessionUser) : null,
|
||||
isAuthenticated: Boolean(sessionUser),
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh auth session:', error)
|
||||
this.setState({
|
||||
...this.state,
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private mapUserToAuthUser(user: User): AuthUser {
|
||||
const level = roleLevels[user.role]
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
name: user.username,
|
||||
role: user.role,
|
||||
level,
|
||||
tenantId: user.tenantId,
|
||||
profilePicture: user.profilePicture,
|
||||
bio: user.bio,
|
||||
isInstanceOwner: user.isInstanceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
private setState(next: AuthState): void {
|
||||
this.state = next
|
||||
this.listeners.forEach((listener) => listener())
|
||||
}
|
||||
}
|
||||
|
||||
export const authStore = new AuthStore()
|
||||
18
frontends/nextjs/src/hooks/auth/utils/map-user.ts
Normal file
18
frontends/nextjs/src/hooks/auth/utils/map-user.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @file map-user.ts
|
||||
* @description Map User type to AuthUser type
|
||||
*/
|
||||
|
||||
import type { User } from '@/lib/level-types'
|
||||
import type { AuthUser } from '../auth-types'
|
||||
import { getRoleLevel } from './role-levels'
|
||||
|
||||
/**
|
||||
* Map a User object to an AuthUser object with level
|
||||
*/
|
||||
export const mapUserToAuthUser = (user: User): AuthUser => {
|
||||
return {
|
||||
...user,
|
||||
level: getRoleLevel(user.role),
|
||||
}
|
||||
}
|
||||
20
frontends/nextjs/src/hooks/auth/utils/role-levels.ts
Normal file
20
frontends/nextjs/src/hooks/auth/utils/role-levels.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file role-levels.ts
|
||||
* @description Role level mappings for authorization
|
||||
*/
|
||||
|
||||
export const roleLevels: Record<string, number> = {
|
||||
public: 1,
|
||||
user: 2,
|
||||
moderator: 3,
|
||||
admin: 4,
|
||||
god: 5,
|
||||
supergod: 6,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the numeric level for a role
|
||||
*/
|
||||
export const getRoleLevel = (role: string): number => {
|
||||
return roleLevels[role] ?? 0
|
||||
}
|
||||
39
frontends/nextjs/src/hooks/data/kv-utils/kv-store.ts
Normal file
39
frontends/nextjs/src/hooks/data/kv-utils/kv-store.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @file kv-store.ts
|
||||
* @description In-memory KV store and subscription management
|
||||
*/
|
||||
|
||||
type Subscriber = (value: unknown) => void
|
||||
|
||||
export const kvStore = new Map<string, unknown>()
|
||||
export const kvSubscribers = new Map<string, Set<Subscriber>>()
|
||||
|
||||
/**
|
||||
* Subscribe to key changes
|
||||
*/
|
||||
export function subscribe(key: string, subscriber: Subscriber): () => void {
|
||||
if (!kvSubscribers.has(key)) {
|
||||
kvSubscribers.set(key, new Set())
|
||||
}
|
||||
kvSubscribers.get(key)!.add(subscriber)
|
||||
|
||||
return () => {
|
||||
const subs = kvSubscribers.get(key)
|
||||
if (subs) {
|
||||
subs.delete(subscriber)
|
||||
if (subs.size === 0) {
|
||||
kvSubscribers.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify subscribers of key change
|
||||
*/
|
||||
export function notifySubscribers(key: string, value: unknown): void {
|
||||
const subs = kvSubscribers.get(key)
|
||||
if (subs) {
|
||||
subs.forEach(fn => fn(value))
|
||||
}
|
||||
}
|
||||
48
frontends/nextjs/src/hooks/data/kv-utils/storage-helpers.ts
Normal file
48
frontends/nextjs/src/hooks/data/kv-utils/storage-helpers.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file storage-helpers.ts
|
||||
* @description Storage access and key management utilities
|
||||
*/
|
||||
|
||||
const STORAGE_PREFIX = 'mb_kv:'
|
||||
|
||||
/**
|
||||
* Get localStorage if available
|
||||
*/
|
||||
export function getLocalStorage(): Storage | null {
|
||||
if (typeof globalThis === 'undefined') return null
|
||||
try {
|
||||
return globalThis.localStorage ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefixed storage key
|
||||
*/
|
||||
export function getStorageKey(key: string): string {
|
||||
return `${STORAGE_PREFIX}${key}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON string
|
||||
*/
|
||||
export function safeParse(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw) as unknown
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely stringify value to JSON
|
||||
*/
|
||||
export function safeStringify(value: unknown): string | null {
|
||||
try {
|
||||
const serialized = JSON.stringify(value)
|
||||
return typeof serialized === 'string' ? serialized : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @file storage-operations.ts
|
||||
* @description KV storage read/write operations
|
||||
*/
|
||||
|
||||
import { kvStore } from './kv-store'
|
||||
import { getLocalStorage, getStorageKey, safeParse, safeStringify } from './storage-helpers'
|
||||
|
||||
/**
|
||||
* Read stored value with migration support
|
||||
*/
|
||||
export function readStoredValue<T>(key: string): T | undefined {
|
||||
if (kvStore.has(key)) {
|
||||
return kvStore.get(key) as T | undefined
|
||||
}
|
||||
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return undefined
|
||||
|
||||
const storageKey = getStorageKey(key)
|
||||
const raw = storage.getItem(storageKey)
|
||||
if (raw !== null) {
|
||||
const parsed = safeParse(raw)
|
||||
kvStore.set(key, parsed)
|
||||
return parsed as T
|
||||
}
|
||||
|
||||
// Legacy migration
|
||||
const legacyRaw = storage.getItem(key)
|
||||
if (legacyRaw === null) return undefined
|
||||
|
||||
const parsedLegacy = safeParse(legacyRaw)
|
||||
kvStore.set(key, parsedLegacy)
|
||||
|
||||
const serialized = safeStringify(parsedLegacy)
|
||||
if (serialized !== null) {
|
||||
try {
|
||||
storage.setItem(storageKey, serialized)
|
||||
storage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Error migrating legacy KV value:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedLegacy as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Write value to storage
|
||||
*/
|
||||
export function writeStoredValue(key: string, value: unknown): void {
|
||||
kvStore.set(key, value)
|
||||
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return
|
||||
|
||||
const storageKey = getStorageKey(key)
|
||||
const serialized = safeStringify(value)
|
||||
|
||||
if (serialized !== null) {
|
||||
try {
|
||||
storage.setItem(storageKey, serialized)
|
||||
} catch (error) {
|
||||
console.error('Error writing KV value:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete value from storage
|
||||
*/
|
||||
export function deleteStoredValue(key: string): void {
|
||||
kvStore.delete(key)
|
||||
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return
|
||||
|
||||
const storageKey = getStorageKey(key)
|
||||
try {
|
||||
storage.removeItem(storageKey)
|
||||
storage.removeItem(key) // Also remove legacy key
|
||||
} catch (error) {
|
||||
console.error('Error deleting KV value:', error)
|
||||
}
|
||||
}
|
||||
@@ -1,226 +1,82 @@
|
||||
/**
|
||||
* Custom useKV hook - replacement for @github/spark/hooks
|
||||
* @file useKV.ts
|
||||
* @description Custom useKV hook - replacement for @github/spark/hooks
|
||||
* Uses in-memory storage with localStorage persistence in the browser
|
||||
* API compatible with @github/spark/hooks
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { subscribe, notifySubscribers } from './kv-utils/kv-store'
|
||||
import { readStoredValue, writeStoredValue, deleteStoredValue } from './kv-utils/storage-operations'
|
||||
|
||||
type Subscriber = (value: unknown) => void
|
||||
|
||||
const STORAGE_PREFIX = 'mb_kv:'
|
||||
|
||||
const kvStore = new Map<string, unknown>()
|
||||
const kvSubscribers = new Map<string, Set<Subscriber>>()
|
||||
let storageListenerRegistered = false
|
||||
|
||||
function getLocalStorage(): Storage | null {
|
||||
if (typeof globalThis === 'undefined') return null
|
||||
try {
|
||||
return globalThis.localStorage ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageKey(key: string): string {
|
||||
return `${STORAGE_PREFIX}${key}`
|
||||
}
|
||||
|
||||
function safeParse(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw) as unknown
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown): string | null {
|
||||
try {
|
||||
const serialized = JSON.stringify(value)
|
||||
return typeof serialized === 'string' ? serialized : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredValue<T>(key: string): T | undefined {
|
||||
if (kvStore.has(key)) {
|
||||
return kvStore.get(key) as T | undefined
|
||||
}
|
||||
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return undefined
|
||||
|
||||
const storageKey = getStorageKey(key)
|
||||
const raw = storage.getItem(storageKey)
|
||||
if (raw !== null) {
|
||||
const parsed = safeParse(raw)
|
||||
kvStore.set(key, parsed)
|
||||
return parsed as T
|
||||
}
|
||||
|
||||
const legacyRaw = storage.getItem(key)
|
||||
if (legacyRaw === null) return undefined
|
||||
|
||||
const parsedLegacy = safeParse(legacyRaw)
|
||||
kvStore.set(key, parsedLegacy)
|
||||
|
||||
const serialized = safeStringify(parsedLegacy)
|
||||
if (serialized !== null) {
|
||||
try {
|
||||
storage.setItem(storageKey, serialized)
|
||||
storage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Error migrating legacy KV value:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedLegacy as T
|
||||
}
|
||||
|
||||
function writeStoredValue<T>(key: string, value: T | undefined): void {
|
||||
const storage = getLocalStorage()
|
||||
const storageKey = getStorageKey(key)
|
||||
|
||||
if (value === undefined) {
|
||||
kvStore.delete(key)
|
||||
storage?.removeItem(storageKey)
|
||||
storage?.removeItem(key)
|
||||
notifySubscribers(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
kvStore.set(key, value)
|
||||
if (!storage) {
|
||||
notifySubscribers(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
const serialized = safeStringify(value)
|
||||
if (serialized === null) {
|
||||
console.error('Error serializing KV value for storage:', key)
|
||||
notifySubscribers(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
storage.setItem(storageKey, serialized)
|
||||
storage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Error persisting KV value:', error)
|
||||
}
|
||||
|
||||
notifySubscribers(key, value)
|
||||
}
|
||||
|
||||
function notifySubscribers(key: string, value: unknown): void {
|
||||
const subscribers = kvSubscribers.get(key)
|
||||
if (!subscribers) return
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber(value)
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToKey(key: string, subscriber: Subscriber): () => void {
|
||||
const subscribers = kvSubscribers.get(key) ?? new Set<Subscriber>()
|
||||
subscribers.add(subscriber)
|
||||
kvSubscribers.set(key, subscribers)
|
||||
|
||||
return () => {
|
||||
subscribers.delete(subscriber)
|
||||
if (subscribers.size === 0) {
|
||||
kvSubscribers.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveStorageKey(storageKey: string): string | null {
|
||||
if (storageKey.startsWith(STORAGE_PREFIX)) {
|
||||
return storageKey.slice(STORAGE_PREFIX.length)
|
||||
}
|
||||
if (kvSubscribers.has(storageKey)) {
|
||||
return storageKey
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function ensureStorageListener(): void {
|
||||
if (storageListenerRegistered) return
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return
|
||||
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return
|
||||
|
||||
window.addEventListener('storage', (event: StorageEvent) => {
|
||||
if (!event.key) return
|
||||
if (event.storageArea && event.storageArea !== storage) return
|
||||
const resolvedKey = resolveStorageKey(event.key)
|
||||
if (!resolvedKey) return
|
||||
|
||||
const nextValue = event.newValue === null ? undefined : safeParse(event.newValue)
|
||||
if (nextValue === undefined) {
|
||||
kvStore.delete(resolvedKey)
|
||||
} else {
|
||||
kvStore.set(resolvedKey, nextValue)
|
||||
}
|
||||
notifySubscribers(resolvedKey, nextValue)
|
||||
})
|
||||
|
||||
function registerStorageListener(): void {
|
||||
if (storageListenerRegistered || typeof window === 'undefined') return
|
||||
storageListenerRegistered = true
|
||||
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (!e.key || !e.key.startsWith('mb_kv:')) return
|
||||
|
||||
const cleanKey = e.key.replace(/^mb_kv:/, '')
|
||||
const newValue = e.newValue ? JSON.parse(e.newValue) : undefined
|
||||
notifySubscribers(cleanKey, newValue)
|
||||
})
|
||||
}
|
||||
|
||||
export function useKV<T = any>(key: string, defaultValue?: T): [T | undefined, (newValueOrUpdater: T | ((prev: T | undefined) => T)) => Promise<void>] {
|
||||
export function useKV<T = any>(
|
||||
key: string,
|
||||
defaultValue?: T
|
||||
): [T | undefined, (newValueOrUpdater: T | ((prev: T | undefined) => T)) => Promise<void>] {
|
||||
const [value, setValue] = useState<T | undefined>(() => {
|
||||
const storedValue = readStoredValue<T>(key)
|
||||
return storedValue !== undefined ? storedValue : defaultValue
|
||||
const stored = readStoredValue<T>(key)
|
||||
return stored !== undefined ? stored : defaultValue
|
||||
})
|
||||
const valueRef = useRef<T | undefined>(value)
|
||||
|
||||
const isFirstMount = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
valueRef.current = value
|
||||
}, [value])
|
||||
registerStorageListener()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
ensureStorageListener()
|
||||
|
||||
const unsubscribe = subscribeToKey(key, (nextValue) => {
|
||||
setValue(nextValue as T | undefined)
|
||||
})
|
||||
|
||||
try {
|
||||
const storedValue = readStoredValue<T>(key)
|
||||
if (storedValue !== undefined) {
|
||||
setValue(storedValue)
|
||||
} else if (defaultValue !== undefined) {
|
||||
writeStoredValue(key, defaultValue)
|
||||
setValue(defaultValue)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading KV value:', err)
|
||||
if (isFirstMount.current) {
|
||||
isFirstMount.current = false
|
||||
return
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
const stored = readStoredValue<T>(key)
|
||||
if (stored !== undefined) {
|
||||
setValue(stored)
|
||||
} else if (defaultValue !== undefined) {
|
||||
setValue(defaultValue)
|
||||
}
|
||||
}, [key, defaultValue])
|
||||
|
||||
// Update value in KV store
|
||||
const updateValue = useCallback(async (newValueOrUpdater: T | ((prev: T | undefined) => T)) => {
|
||||
try {
|
||||
// Handle updater function
|
||||
const currentValue = kvStore.has(key) ? (kvStore.get(key) as T | undefined) : valueRef.current
|
||||
const newValue = typeof newValueOrUpdater === 'function'
|
||||
? (newValueOrUpdater as (prev: T | undefined) => T)(currentValue)
|
||||
: newValueOrUpdater
|
||||
|
||||
writeStoredValue(key, newValue)
|
||||
setValue(newValue)
|
||||
} catch (err) {
|
||||
console.error('Error saving KV value:', err)
|
||||
}
|
||||
useEffect(() => {
|
||||
return subscribe(key, (newValue) => {
|
||||
setValue(newValue as T | undefined)
|
||||
})
|
||||
}, [key])
|
||||
|
||||
return [value, updateValue]
|
||||
const setKV = useCallback(
|
||||
async (newValueOrUpdater: T | ((prev: T | undefined) => T)) => {
|
||||
const newValue =
|
||||
typeof newValueOrUpdater === 'function'
|
||||
? (newValueOrUpdater as (prev: T | undefined) => T)(value)
|
||||
: newValueOrUpdater
|
||||
|
||||
setValue(newValue)
|
||||
writeStoredValue(key, newValue)
|
||||
notifySubscribers(key, newValue)
|
||||
},
|
||||
[key, value]
|
||||
)
|
||||
|
||||
return [value, setKV]
|
||||
}
|
||||
|
||||
// Alias for compatibility
|
||||
export { useKV as default }
|
||||
export function deleteKV(key: string): void {
|
||||
deleteStoredValue(key)
|
||||
notifySubscribers(key, undefined)
|
||||
}
|
||||
|
||||
226
frontends/nextjs/src/hooks/data/useKV.ts.backup
Normal file
226
frontends/nextjs/src/hooks/data/useKV.ts.backup
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Custom useKV hook - replacement for @github/spark/hooks
|
||||
* Uses in-memory storage with localStorage persistence in the browser
|
||||
* API compatible with @github/spark/hooks
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
|
||||
type Subscriber = (value: unknown) => void
|
||||
|
||||
const STORAGE_PREFIX = 'mb_kv:'
|
||||
|
||||
const kvStore = new Map<string, unknown>()
|
||||
const kvSubscribers = new Map<string, Set<Subscriber>>()
|
||||
let storageListenerRegistered = false
|
||||
|
||||
function getLocalStorage(): Storage | null {
|
||||
if (typeof globalThis === 'undefined') return null
|
||||
try {
|
||||
return globalThis.localStorage ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageKey(key: string): string {
|
||||
return `${STORAGE_PREFIX}${key}`
|
||||
}
|
||||
|
||||
function safeParse(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw) as unknown
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown): string | null {
|
||||
try {
|
||||
const serialized = JSON.stringify(value)
|
||||
return typeof serialized === 'string' ? serialized : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredValue<T>(key: string): T | undefined {
|
||||
if (kvStore.has(key)) {
|
||||
return kvStore.get(key) as T | undefined
|
||||
}
|
||||
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return undefined
|
||||
|
||||
const storageKey = getStorageKey(key)
|
||||
const raw = storage.getItem(storageKey)
|
||||
if (raw !== null) {
|
||||
const parsed = safeParse(raw)
|
||||
kvStore.set(key, parsed)
|
||||
return parsed as T
|
||||
}
|
||||
|
||||
const legacyRaw = storage.getItem(key)
|
||||
if (legacyRaw === null) return undefined
|
||||
|
||||
const parsedLegacy = safeParse(legacyRaw)
|
||||
kvStore.set(key, parsedLegacy)
|
||||
|
||||
const serialized = safeStringify(parsedLegacy)
|
||||
if (serialized !== null) {
|
||||
try {
|
||||
storage.setItem(storageKey, serialized)
|
||||
storage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Error migrating legacy KV value:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedLegacy as T
|
||||
}
|
||||
|
||||
function writeStoredValue<T>(key: string, value: T | undefined): void {
|
||||
const storage = getLocalStorage()
|
||||
const storageKey = getStorageKey(key)
|
||||
|
||||
if (value === undefined) {
|
||||
kvStore.delete(key)
|
||||
storage?.removeItem(storageKey)
|
||||
storage?.removeItem(key)
|
||||
notifySubscribers(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
kvStore.set(key, value)
|
||||
if (!storage) {
|
||||
notifySubscribers(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
const serialized = safeStringify(value)
|
||||
if (serialized === null) {
|
||||
console.error('Error serializing KV value for storage:', key)
|
||||
notifySubscribers(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
storage.setItem(storageKey, serialized)
|
||||
storage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Error persisting KV value:', error)
|
||||
}
|
||||
|
||||
notifySubscribers(key, value)
|
||||
}
|
||||
|
||||
function notifySubscribers(key: string, value: unknown): void {
|
||||
const subscribers = kvSubscribers.get(key)
|
||||
if (!subscribers) return
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber(value)
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToKey(key: string, subscriber: Subscriber): () => void {
|
||||
const subscribers = kvSubscribers.get(key) ?? new Set<Subscriber>()
|
||||
subscribers.add(subscriber)
|
||||
kvSubscribers.set(key, subscribers)
|
||||
|
||||
return () => {
|
||||
subscribers.delete(subscriber)
|
||||
if (subscribers.size === 0) {
|
||||
kvSubscribers.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveStorageKey(storageKey: string): string | null {
|
||||
if (storageKey.startsWith(STORAGE_PREFIX)) {
|
||||
return storageKey.slice(STORAGE_PREFIX.length)
|
||||
}
|
||||
if (kvSubscribers.has(storageKey)) {
|
||||
return storageKey
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function ensureStorageListener(): void {
|
||||
if (storageListenerRegistered) return
|
||||
const storage = getLocalStorage()
|
||||
if (!storage) return
|
||||
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return
|
||||
|
||||
window.addEventListener('storage', (event: StorageEvent) => {
|
||||
if (!event.key) return
|
||||
if (event.storageArea && event.storageArea !== storage) return
|
||||
const resolvedKey = resolveStorageKey(event.key)
|
||||
if (!resolvedKey) return
|
||||
|
||||
const nextValue = event.newValue === null ? undefined : safeParse(event.newValue)
|
||||
if (nextValue === undefined) {
|
||||
kvStore.delete(resolvedKey)
|
||||
} else {
|
||||
kvStore.set(resolvedKey, nextValue)
|
||||
}
|
||||
notifySubscribers(resolvedKey, nextValue)
|
||||
})
|
||||
|
||||
storageListenerRegistered = true
|
||||
}
|
||||
|
||||
export function useKV<T = any>(key: string, defaultValue?: T): [T | undefined, (newValueOrUpdater: T | ((prev: T | undefined) => T)) => Promise<void>] {
|
||||
const [value, setValue] = useState<T | undefined>(() => {
|
||||
const storedValue = readStoredValue<T>(key)
|
||||
return storedValue !== undefined ? storedValue : defaultValue
|
||||
})
|
||||
const valueRef = useRef<T | undefined>(value)
|
||||
|
||||
useEffect(() => {
|
||||
valueRef.current = value
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
ensureStorageListener()
|
||||
|
||||
const unsubscribe = subscribeToKey(key, (nextValue) => {
|
||||
setValue(nextValue as T | undefined)
|
||||
})
|
||||
|
||||
try {
|
||||
const storedValue = readStoredValue<T>(key)
|
||||
if (storedValue !== undefined) {
|
||||
setValue(storedValue)
|
||||
} else if (defaultValue !== undefined) {
|
||||
writeStoredValue(key, defaultValue)
|
||||
setValue(defaultValue)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading KV value:', err)
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [key, defaultValue])
|
||||
|
||||
// Update value in KV store
|
||||
const updateValue = useCallback(async (newValueOrUpdater: T | ((prev: T | undefined) => T)) => {
|
||||
try {
|
||||
// Handle updater function
|
||||
const currentValue = kvStore.has(key) ? (kvStore.get(key) as T | undefined) : valueRef.current
|
||||
const newValue = typeof newValueOrUpdater === 'function'
|
||||
? (newValueOrUpdater as (prev: T | undefined) => T)(currentValue)
|
||||
: newValueOrUpdater
|
||||
|
||||
writeStoredValue(key, newValue)
|
||||
setValue(newValue)
|
||||
} catch (err) {
|
||||
console.error('Error saving KV value:', err)
|
||||
}
|
||||
}, [key])
|
||||
|
||||
return [value, updateValue]
|
||||
}
|
||||
|
||||
// Alias for compatibility
|
||||
export { useKV as default }
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file basic-examples.ts
|
||||
* @description Basic Lua examples for beginners
|
||||
*/
|
||||
|
||||
export const basicExamples = {
|
||||
basic: `-- Basic Hello World
|
||||
log("Hello from Lua!")
|
||||
return { message = "Success", timestamp = os.time() }`,
|
||||
|
||||
conditional: `-- Conditional Logic Example
|
||||
-- Workflow decision making
|
||||
local status = context.data.status or "pending"
|
||||
|
||||
if status == "approved" then
|
||||
log("Processing approved workflow")
|
||||
return { action = "continue", message = "Approved - proceeding" }
|
||||
elseif status == "rejected" then
|
||||
log("Halting rejected workflow")
|
||||
return { action = "halt", message = "Rejected - stopping" }
|
||||
else
|
||||
log("Waiting for review")
|
||||
return { action = "wait", message = "Pending review" }
|
||||
end`,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @file data-examples.ts
|
||||
* @description Data processing and transformation Lua examples
|
||||
*/
|
||||
|
||||
export const dataExamples = {
|
||||
dataProcessing: `-- Data Processing Example
|
||||
-- Access parameters via context.data
|
||||
log("Processing data...")
|
||||
|
||||
local input = context.data or {}
|
||||
local result = {
|
||||
count = 0,
|
||||
items = {}
|
||||
}
|
||||
|
||||
if input.items then
|
||||
for i, item in ipairs(input.items) do
|
||||
if item.value > 10 then
|
||||
result.count = result.count + 1
|
||||
table.insert(result.items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log("Found " .. result.count .. " items")
|
||||
return result`,
|
||||
|
||||
transformation: `-- Data Transformation Example
|
||||
-- Transform input data structure
|
||||
local input = context.data or {}
|
||||
|
||||
local output = {
|
||||
fullName = (input.firstName or "") .. " " .. (input.lastName or ""),
|
||||
displayAge = tostring(input.age or 0) .. " years old",
|
||||
status = input.isActive and "Active" or "Inactive",
|
||||
metadata = {
|
||||
processedAt = os.time(),
|
||||
processedBy = "lua_transform"
|
||||
}
|
||||
}
|
||||
|
||||
log("Transformed data for: " .. output.fullName)
|
||||
return output`,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file validation-examples.ts
|
||||
* @description Validation and business logic Lua examples
|
||||
*/
|
||||
|
||||
export const validationExamples = {
|
||||
validation: `-- Validation Example
|
||||
-- Returns true/false based on validation rules
|
||||
local data = context.data or {}
|
||||
|
||||
if not data.email then
|
||||
log("Error: Email is required")
|
||||
return { valid = false, error = "Email is required" }
|
||||
end
|
||||
|
||||
if not string.match(data.email, "@") then
|
||||
log("Error: Invalid email format")
|
||||
return { valid = false, error = "Invalid email format" }
|
||||
end
|
||||
|
||||
if data.age and data.age < 18 then
|
||||
log("Error: Must be 18 or older")
|
||||
return { valid = false, error = "Must be 18 or older" }
|
||||
end
|
||||
|
||||
log("Validation passed")
|
||||
return { valid = true }`,
|
||||
|
||||
calculation: `-- Complex Calculation Example
|
||||
-- Perform business logic calculations
|
||||
local data = context.data or {}
|
||||
|
||||
local subtotal = data.price or 0
|
||||
local quantity = data.quantity or 1
|
||||
local discount = data.discount or 0
|
||||
|
||||
local total = subtotal * quantity
|
||||
local discountAmount = total * (discount / 100)
|
||||
local finalTotal = total - discountAmount
|
||||
|
||||
local taxRate = 0.08
|
||||
local taxAmount = finalTotal * taxRate
|
||||
local grandTotal = finalTotal + taxAmount
|
||||
|
||||
log("Calculation complete:")
|
||||
log(" Subtotal: $" .. string.format("%.2f", subtotal))
|
||||
log(" Quantity: " .. quantity)
|
||||
log(" Discount: " .. discount .. "%")
|
||||
log(" Tax: $" .. string.format("%.2f", taxAmount))
|
||||
log(" Grand Total: $" .. string.format("%.2f", grandTotal))
|
||||
|
||||
return {
|
||||
subtotal = subtotal,
|
||||
quantity = quantity,
|
||||
discount = discount,
|
||||
discountAmount = discountAmount,
|
||||
taxAmount = taxAmount,
|
||||
grandTotal = grandTotal
|
||||
}`,
|
||||
}
|
||||
@@ -1,210 +1,17 @@
|
||||
/**
|
||||
* @file lua-examples-data.ts
|
||||
* @description Aggregated Lua examples for the editor
|
||||
*
|
||||
* This file imports and re-exports examples from organized categories.
|
||||
* Each category is in its own file for better maintainability.
|
||||
*/
|
||||
|
||||
import { basicExamples } from './categories/basic-examples'
|
||||
import { dataExamples } from './categories/data-examples'
|
||||
import { validationExamples } from './categories/validation-examples'
|
||||
|
||||
export const LUA_EXAMPLES = {
|
||||
basic: `-- Basic Hello World
|
||||
log("Hello from Lua!")
|
||||
return { message = "Success", timestamp = os.time() }`,
|
||||
|
||||
dataProcessing: `-- Data Processing Example
|
||||
-- Access parameters via context.data
|
||||
log("Processing data...")
|
||||
|
||||
local input = context.data or {}
|
||||
local result = {
|
||||
count = 0,
|
||||
items = {}
|
||||
...basicExamples,
|
||||
...dataExamples,
|
||||
...validationExamples,
|
||||
}
|
||||
|
||||
if input.items then
|
||||
for i, item in ipairs(input.items) do
|
||||
if item.value > 10 then
|
||||
result.count = result.count + 1
|
||||
table.insert(result.items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log("Found " .. result.count .. " items")
|
||||
return result`,
|
||||
|
||||
validation: `-- Validation Example
|
||||
-- Returns true/false based on validation rules
|
||||
local data = context.data or {}
|
||||
|
||||
if not data.email then
|
||||
log("Error: Email is required")
|
||||
return { valid = false, error = "Email is required" }
|
||||
end
|
||||
|
||||
if not string.match(data.email, "@") then
|
||||
log("Error: Invalid email format")
|
||||
return { valid = false, error = "Invalid email format" }
|
||||
end
|
||||
|
||||
if data.age and data.age < 18 then
|
||||
log("Error: Must be 18 or older")
|
||||
return { valid = false, error = "Must be 18 or older" }
|
||||
end
|
||||
|
||||
log("Validation passed")
|
||||
return { valid = true }`,
|
||||
|
||||
transformation: `-- Data Transformation Example
|
||||
-- Transform input data structure
|
||||
local input = context.data or {}
|
||||
|
||||
local output = {
|
||||
fullName = (input.firstName or "") .. " " .. (input.lastName or ""),
|
||||
displayAge = tostring(input.age or 0) .. " years old",
|
||||
status = input.isActive and "Active" or "Inactive",
|
||||
metadata = {
|
||||
processedAt = os.time(),
|
||||
processedBy = "lua_transform"
|
||||
}
|
||||
}
|
||||
|
||||
log("Transformed data for: " .. output.fullName)
|
||||
return output`,
|
||||
|
||||
calculation: `-- Complex Calculation Example
|
||||
-- Perform business logic calculations
|
||||
local data = context.data or {}
|
||||
|
||||
local subtotal = data.price or 0
|
||||
local quantity = data.quantity or 1
|
||||
local discount = data.discount or 0
|
||||
|
||||
local total = subtotal * quantity
|
||||
local discountAmount = total * (discount / 100)
|
||||
local finalTotal = total - discountAmount
|
||||
|
||||
local taxRate = 0.08
|
||||
local taxAmount = finalTotal * taxRate
|
||||
local grandTotal = finalTotal + taxAmount
|
||||
|
||||
log("Calculation complete:")
|
||||
log(" Subtotal: $" .. string.format("%.2f", subtotal))
|
||||
log(" Quantity: " .. quantity)
|
||||
log(" Discount: " .. discount .. "%")
|
||||
log(" Tax: $" .. string.format("%.2f", taxAmount))
|
||||
log(" Grand Total: $" .. string.format("%.2f", grandTotal))
|
||||
|
||||
return {
|
||||
subtotal = subtotal,
|
||||
quantity = quantity,
|
||||
discount = discount,
|
||||
discountAmount = discountAmount,
|
||||
taxAmount = taxAmount,
|
||||
grandTotal = grandTotal
|
||||
}`,
|
||||
|
||||
conditional: `-- Conditional Logic Example
|
||||
-- Workflow decision making
|
||||
local data = context.data or {}
|
||||
local user = context.user or {}
|
||||
|
||||
log("Evaluating conditions...")
|
||||
|
||||
if user.role == "admin" then
|
||||
log("Admin user - granting full access")
|
||||
return {
|
||||
approved = true,
|
||||
accessLevel = "full",
|
||||
reason = "Admin override"
|
||||
}
|
||||
end
|
||||
|
||||
if data.score and data.score >= 80 then
|
||||
log("Score passed threshold")
|
||||
return {
|
||||
approved = true,
|
||||
accessLevel = "standard",
|
||||
reason = "Score requirement met"
|
||||
}
|
||||
end
|
||||
|
||||
if data.verified == true then
|
||||
log("User is verified")
|
||||
return {
|
||||
approved = true,
|
||||
accessLevel = "limited",
|
||||
reason = "Verified user"
|
||||
}
|
||||
end
|
||||
|
||||
log("Conditions not met")
|
||||
return {
|
||||
approved = false,
|
||||
accessLevel = "none",
|
||||
reason = "Requirements not satisfied"
|
||||
}`,
|
||||
|
||||
arrayOperations: `-- Array Operations Example
|
||||
-- Working with lists and tables
|
||||
local data = context.data or {}
|
||||
local numbers = data.numbers or {1, 2, 3, 4, 5}
|
||||
|
||||
local sum = 0
|
||||
local max = numbers[1] or 0
|
||||
local min = numbers[1] or 0
|
||||
|
||||
for i, num in ipairs(numbers) do
|
||||
sum = sum + num
|
||||
if num > max then max = num end
|
||||
if num < min then min = num end
|
||||
end
|
||||
|
||||
local average = sum / #numbers
|
||||
|
||||
log("Array statistics:")
|
||||
log(" Count: " .. #numbers)
|
||||
log(" Sum: " .. sum)
|
||||
log(" Average: " .. string.format("%.2f", average))
|
||||
log(" Min: " .. min)
|
||||
log(" Max: " .. max)
|
||||
|
||||
return {
|
||||
count = #numbers,
|
||||
sum = sum,
|
||||
average = average,
|
||||
min = min,
|
||||
max = max,
|
||||
values = numbers
|
||||
}`,
|
||||
|
||||
stringManipulation: `-- String Manipulation Example
|
||||
-- Text processing and formatting
|
||||
local data = context.data or {}
|
||||
local text = data.text or "hello world"
|
||||
|
||||
local upperText = string.upper(text)
|
||||
local lowerText = string.lower(text)
|
||||
local length = string.len(text)
|
||||
|
||||
local words = {}
|
||||
for word in string.gmatch(text, "%S+") do
|
||||
table.insert(words, word)
|
||||
end
|
||||
|
||||
local reversed = string.reverse(text)
|
||||
|
||||
local hasDigit = string.match(text, "%d") ~= nil
|
||||
local hasSpecial = string.match(text, "[^%w%s]") ~= nil
|
||||
|
||||
log("Text analysis complete:")
|
||||
log(" Length: " .. length)
|
||||
log(" Words: " .. #words)
|
||||
log(" Has digits: " .. tostring(hasDigit))
|
||||
|
||||
return {
|
||||
original = text,
|
||||
upper = upperText,
|
||||
lower = lowerText,
|
||||
length = length,
|
||||
wordCount = #words,
|
||||
words = words,
|
||||
reversed = reversed,
|
||||
hasDigit = hasDigit,
|
||||
hasSpecial = hasSpecial
|
||||
}`
|
||||
} as const
|
||||
|
||||
export type LuaExampleKey = keyof typeof LUA_EXAMPLES
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
export const LUA_EXAMPLES = {
|
||||
basic: `-- Basic Hello World
|
||||
log("Hello from Lua!")
|
||||
return { message = "Success", timestamp = os.time() }`,
|
||||
|
||||
dataProcessing: `-- Data Processing Example
|
||||
-- Access parameters via context.data
|
||||
log("Processing data...")
|
||||
|
||||
local input = context.data or {}
|
||||
local result = {
|
||||
count = 0,
|
||||
items = {}
|
||||
}
|
||||
|
||||
if input.items then
|
||||
for i, item in ipairs(input.items) do
|
||||
if item.value > 10 then
|
||||
result.count = result.count + 1
|
||||
table.insert(result.items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log("Found " .. result.count .. " items")
|
||||
return result`,
|
||||
|
||||
validation: `-- Validation Example
|
||||
-- Returns true/false based on validation rules
|
||||
local data = context.data or {}
|
||||
|
||||
if not data.email then
|
||||
log("Error: Email is required")
|
||||
return { valid = false, error = "Email is required" }
|
||||
end
|
||||
|
||||
if not string.match(data.email, "@") then
|
||||
log("Error: Invalid email format")
|
||||
return { valid = false, error = "Invalid email format" }
|
||||
end
|
||||
|
||||
if data.age and data.age < 18 then
|
||||
log("Error: Must be 18 or older")
|
||||
return { valid = false, error = "Must be 18 or older" }
|
||||
end
|
||||
|
||||
log("Validation passed")
|
||||
return { valid = true }`,
|
||||
|
||||
transformation: `-- Data Transformation Example
|
||||
-- Transform input data structure
|
||||
local input = context.data or {}
|
||||
|
||||
local output = {
|
||||
fullName = (input.firstName or "") .. " " .. (input.lastName or ""),
|
||||
displayAge = tostring(input.age or 0) .. " years old",
|
||||
status = input.isActive and "Active" or "Inactive",
|
||||
metadata = {
|
||||
processedAt = os.time(),
|
||||
processedBy = "lua_transform"
|
||||
}
|
||||
}
|
||||
|
||||
log("Transformed data for: " .. output.fullName)
|
||||
return output`,
|
||||
|
||||
calculation: `-- Complex Calculation Example
|
||||
-- Perform business logic calculations
|
||||
local data = context.data or {}
|
||||
|
||||
local subtotal = data.price or 0
|
||||
local quantity = data.quantity or 1
|
||||
local discount = data.discount or 0
|
||||
|
||||
local total = subtotal * quantity
|
||||
local discountAmount = total * (discount / 100)
|
||||
local finalTotal = total - discountAmount
|
||||
|
||||
local taxRate = 0.08
|
||||
local taxAmount = finalTotal * taxRate
|
||||
local grandTotal = finalTotal + taxAmount
|
||||
|
||||
log("Calculation complete:")
|
||||
log(" Subtotal: $" .. string.format("%.2f", subtotal))
|
||||
log(" Quantity: " .. quantity)
|
||||
log(" Discount: " .. discount .. "%")
|
||||
log(" Tax: $" .. string.format("%.2f", taxAmount))
|
||||
log(" Grand Total: $" .. string.format("%.2f", grandTotal))
|
||||
|
||||
return {
|
||||
subtotal = subtotal,
|
||||
quantity = quantity,
|
||||
discount = discount,
|
||||
discountAmount = discountAmount,
|
||||
taxAmount = taxAmount,
|
||||
grandTotal = grandTotal
|
||||
}`,
|
||||
|
||||
conditional: `-- Conditional Logic Example
|
||||
-- Workflow decision making
|
||||
local data = context.data or {}
|
||||
local user = context.user or {}
|
||||
|
||||
log("Evaluating conditions...")
|
||||
|
||||
if user.role == "admin" then
|
||||
log("Admin user - granting full access")
|
||||
return {
|
||||
approved = true,
|
||||
accessLevel = "full",
|
||||
reason = "Admin override"
|
||||
}
|
||||
end
|
||||
|
||||
if data.score and data.score >= 80 then
|
||||
log("Score passed threshold")
|
||||
return {
|
||||
approved = true,
|
||||
accessLevel = "standard",
|
||||
reason = "Score requirement met"
|
||||
}
|
||||
end
|
||||
|
||||
if data.verified == true then
|
||||
log("User is verified")
|
||||
return {
|
||||
approved = true,
|
||||
accessLevel = "limited",
|
||||
reason = "Verified user"
|
||||
}
|
||||
end
|
||||
|
||||
log("Conditions not met")
|
||||
return {
|
||||
approved = false,
|
||||
accessLevel = "none",
|
||||
reason = "Requirements not satisfied"
|
||||
}`,
|
||||
|
||||
arrayOperations: `-- Array Operations Example
|
||||
-- Working with lists and tables
|
||||
local data = context.data or {}
|
||||
local numbers = data.numbers or {1, 2, 3, 4, 5}
|
||||
|
||||
local sum = 0
|
||||
local max = numbers[1] or 0
|
||||
local min = numbers[1] or 0
|
||||
|
||||
for i, num in ipairs(numbers) do
|
||||
sum = sum + num
|
||||
if num > max then max = num end
|
||||
if num < min then min = num end
|
||||
end
|
||||
|
||||
local average = sum / #numbers
|
||||
|
||||
log("Array statistics:")
|
||||
log(" Count: " .. #numbers)
|
||||
log(" Sum: " .. sum)
|
||||
log(" Average: " .. string.format("%.2f", average))
|
||||
log(" Min: " .. min)
|
||||
log(" Max: " .. max)
|
||||
|
||||
return {
|
||||
count = #numbers,
|
||||
sum = sum,
|
||||
average = average,
|
||||
min = min,
|
||||
max = max,
|
||||
values = numbers
|
||||
}`,
|
||||
|
||||
stringManipulation: `-- String Manipulation Example
|
||||
-- Text processing and formatting
|
||||
local data = context.data or {}
|
||||
local text = data.text or "hello world"
|
||||
|
||||
local upperText = string.upper(text)
|
||||
local lowerText = string.lower(text)
|
||||
local length = string.len(text)
|
||||
|
||||
local words = {}
|
||||
for word in string.gmatch(text, "%S+") do
|
||||
table.insert(words, word)
|
||||
end
|
||||
|
||||
local reversed = string.reverse(text)
|
||||
|
||||
local hasDigit = string.match(text, "%d") ~= nil
|
||||
local hasSpecial = string.match(text, "[^%w%s]") ~= nil
|
||||
|
||||
log("Text analysis complete:")
|
||||
log(" Length: " .. length)
|
||||
log(" Words: " .. #words)
|
||||
log(" Has digits: " .. tostring(hasDigit))
|
||||
|
||||
return {
|
||||
original = text,
|
||||
upper = upperText,
|
||||
lower = lowerText,
|
||||
length = length,
|
||||
wordCount = #words,
|
||||
words = words,
|
||||
reversed = reversed,
|
||||
hasDigit = hasDigit,
|
||||
hasSpecial = hasSpecial
|
||||
}`
|
||||
} as const
|
||||
|
||||
export type LuaExampleKey = keyof typeof LUA_EXAMPLES
|
||||
@@ -1,208 +1,19 @@
|
||||
import type { PackageDefinition } from './types'
|
||||
/**
|
||||
* @file default-packages.ts
|
||||
* @description Default package definitions aggregated from individual package modules
|
||||
*/
|
||||
|
||||
export type PackageSeedConfig = {
|
||||
metadata: Omit<PackageDefinition, 'components' | 'scripts' | 'scriptFiles' | 'examples'>
|
||||
components: any[]
|
||||
examples: any
|
||||
}
|
||||
import type { PackageSeedConfig } from './types'
|
||||
import { adminDialog } from './packages/admin-dialog'
|
||||
import { dataTable } from './packages/data-table'
|
||||
import { formBuilder } from './packages/form-builder'
|
||||
import { navMenu } from './packages/nav-menu'
|
||||
|
||||
const adminDialogComponents: any[] = []
|
||||
const adminDialogMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'admin_dialog',
|
||||
name: 'Admin Dialog',
|
||||
version: '1.0.0',
|
||||
description: 'Admin dialog package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const adminDialogExamples: any = {}
|
||||
|
||||
const dataTableComponents: any[] = []
|
||||
const dataTableMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'data_table',
|
||||
name: 'Data Table',
|
||||
version: '1.0.0',
|
||||
description: 'Data table package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const dataTableExamples: any = {}
|
||||
|
||||
const formBuilderComponents: any[] = []
|
||||
const formBuilderMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'form_builder',
|
||||
name: 'Form Builder',
|
||||
version: '1.0.0',
|
||||
description: 'Form builder package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const formBuilderExamples: any = {}
|
||||
|
||||
const navMenuComponents: any[] = []
|
||||
const navMenuMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'nav_menu',
|
||||
name: 'Nav Menu',
|
||||
version: '1.0.0',
|
||||
description: 'Navigation menu package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const navMenuExamples: any = {}
|
||||
|
||||
const dashboardComponents: any[] = []
|
||||
const dashboardMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
version: '1.0.0',
|
||||
description: 'Dashboard package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const dashboardExamples: any = {}
|
||||
|
||||
const notificationCenterComponents: any[] = []
|
||||
const notificationCenterMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'notification_center',
|
||||
name: 'Notification Center',
|
||||
version: '1.0.0',
|
||||
description: 'Notification center package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const notificationCenterExamples: any = {}
|
||||
|
||||
const socialHubComponents: any[] = []
|
||||
const socialHubMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'social_hub',
|
||||
name: 'Social Hub',
|
||||
version: '1.0.0',
|
||||
description: 'Social feed package with live rooms and creator updates',
|
||||
author: 'MetaBuilder',
|
||||
category: 'social',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const socialHubExamples: any = {}
|
||||
|
||||
const codegenStudioComponents: any[] = []
|
||||
const codegenStudioMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'codegen_studio',
|
||||
name: 'Codegen Studio',
|
||||
version: '1.0.0',
|
||||
description: 'Template-driven code generation studio with zip exports',
|
||||
author: 'MetaBuilder',
|
||||
category: 'tools',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const codegenStudioExamples: any = {}
|
||||
|
||||
const forumForgeComponents: any[] = []
|
||||
const forumForgeMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'forum_forge',
|
||||
name: 'Forum Forge',
|
||||
version: '1.0.0',
|
||||
description: 'Modern forum starter with categories, threads, and moderation',
|
||||
author: 'MetaBuilder',
|
||||
category: 'social',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const forumForgeExamples: any = {}
|
||||
|
||||
const arcadeLobbyComponents: any[] = []
|
||||
const arcadeLobbyMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'arcade_lobby',
|
||||
name: 'Arcade Lobby',
|
||||
version: '1.0.0',
|
||||
description: 'Gaming lobby with queues, tournaments, and party setup',
|
||||
author: 'MetaBuilder',
|
||||
category: 'gaming',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const arcadeLobbyExamples: any = {}
|
||||
|
||||
const streamCastComponents: any[] = []
|
||||
const streamCastMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'stream_cast',
|
||||
name: 'Stream Cast',
|
||||
version: '1.0.0',
|
||||
description: 'Live streaming control room with schedules and scene control',
|
||||
author: 'MetaBuilder',
|
||||
category: 'media',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const streamCastExamples: any = {}
|
||||
export type { PackageSeedConfig }
|
||||
|
||||
export const DEFAULT_PACKAGES: Record<string, PackageSeedConfig> = {
|
||||
admin_dialog: {
|
||||
metadata: adminDialogMetadata,
|
||||
components: adminDialogComponents,
|
||||
examples: adminDialogExamples,
|
||||
},
|
||||
data_table: {
|
||||
metadata: dataTableMetadata,
|
||||
components: dataTableComponents,
|
||||
examples: dataTableExamples,
|
||||
},
|
||||
form_builder: {
|
||||
metadata: formBuilderMetadata,
|
||||
components: formBuilderComponents,
|
||||
examples: formBuilderExamples,
|
||||
},
|
||||
nav_menu: {
|
||||
metadata: navMenuMetadata,
|
||||
components: navMenuComponents,
|
||||
examples: navMenuExamples,
|
||||
},
|
||||
dashboard: {
|
||||
metadata: dashboardMetadata,
|
||||
components: dashboardComponents,
|
||||
examples: dashboardExamples,
|
||||
},
|
||||
notification_center: {
|
||||
metadata: notificationCenterMetadata,
|
||||
components: notificationCenterComponents,
|
||||
examples: notificationCenterExamples,
|
||||
},
|
||||
social_hub: {
|
||||
metadata: socialHubMetadata,
|
||||
components: socialHubComponents,
|
||||
examples: socialHubExamples,
|
||||
},
|
||||
codegen_studio: {
|
||||
metadata: codegenStudioMetadata,
|
||||
components: codegenStudioComponents,
|
||||
examples: codegenStudioExamples,
|
||||
},
|
||||
forum_forge: {
|
||||
metadata: forumForgeMetadata,
|
||||
components: forumForgeComponents,
|
||||
examples: forumForgeExamples,
|
||||
},
|
||||
arcade_lobby: {
|
||||
metadata: arcadeLobbyMetadata,
|
||||
components: arcadeLobbyComponents,
|
||||
examples: arcadeLobbyExamples,
|
||||
},
|
||||
stream_cast: {
|
||||
metadata: streamCastMetadata,
|
||||
components: streamCastComponents,
|
||||
examples: streamCastExamples,
|
||||
},
|
||||
admin_dialog: adminDialog,
|
||||
data_table: dataTable,
|
||||
form_builder: formBuilder,
|
||||
nav_menu: navMenu,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { PackageDefinition } from './types'
|
||||
|
||||
export type PackageSeedConfig = {
|
||||
metadata: Omit<PackageDefinition, 'components' | 'scripts' | 'scriptFiles' | 'examples'>
|
||||
components: any[]
|
||||
examples: any
|
||||
}
|
||||
|
||||
const adminDialogComponents: any[] = []
|
||||
const adminDialogMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'admin_dialog',
|
||||
name: 'Admin Dialog',
|
||||
version: '1.0.0',
|
||||
description: 'Admin dialog package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const adminDialogExamples: any = {}
|
||||
|
||||
const dataTableComponents: any[] = []
|
||||
const dataTableMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'data_table',
|
||||
name: 'Data Table',
|
||||
version: '1.0.0',
|
||||
description: 'Data table package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const dataTableExamples: any = {}
|
||||
|
||||
const formBuilderComponents: any[] = []
|
||||
const formBuilderMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'form_builder',
|
||||
name: 'Form Builder',
|
||||
version: '1.0.0',
|
||||
description: 'Form builder package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const formBuilderExamples: any = {}
|
||||
|
||||
const navMenuComponents: any[] = []
|
||||
const navMenuMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'nav_menu',
|
||||
name: 'Nav Menu',
|
||||
version: '1.0.0',
|
||||
description: 'Navigation menu package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const navMenuExamples: any = {}
|
||||
|
||||
const dashboardComponents: any[] = []
|
||||
const dashboardMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
version: '1.0.0',
|
||||
description: 'Dashboard package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const dashboardExamples: any = {}
|
||||
|
||||
const notificationCenterComponents: any[] = []
|
||||
const notificationCenterMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'notification_center',
|
||||
name: 'Notification Center',
|
||||
version: '1.0.0',
|
||||
description: 'Notification center package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const notificationCenterExamples: any = {}
|
||||
|
||||
const socialHubComponents: any[] = []
|
||||
const socialHubMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'social_hub',
|
||||
name: 'Social Hub',
|
||||
version: '1.0.0',
|
||||
description: 'Social feed package with live rooms and creator updates',
|
||||
author: 'MetaBuilder',
|
||||
category: 'social',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const socialHubExamples: any = {}
|
||||
|
||||
const codegenStudioComponents: any[] = []
|
||||
const codegenStudioMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'codegen_studio',
|
||||
name: 'Codegen Studio',
|
||||
version: '1.0.0',
|
||||
description: 'Template-driven code generation studio with zip exports',
|
||||
author: 'MetaBuilder',
|
||||
category: 'tools',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const codegenStudioExamples: any = {}
|
||||
|
||||
const forumForgeComponents: any[] = []
|
||||
const forumForgeMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'forum_forge',
|
||||
name: 'Forum Forge',
|
||||
version: '1.0.0',
|
||||
description: 'Modern forum starter with categories, threads, and moderation',
|
||||
author: 'MetaBuilder',
|
||||
category: 'social',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const forumForgeExamples: any = {}
|
||||
|
||||
const arcadeLobbyComponents: any[] = []
|
||||
const arcadeLobbyMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'arcade_lobby',
|
||||
name: 'Arcade Lobby',
|
||||
version: '1.0.0',
|
||||
description: 'Gaming lobby with queues, tournaments, and party setup',
|
||||
author: 'MetaBuilder',
|
||||
category: 'gaming',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const arcadeLobbyExamples: any = {}
|
||||
|
||||
const streamCastComponents: any[] = []
|
||||
const streamCastMetadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'stream_cast',
|
||||
name: 'Stream Cast',
|
||||
version: '1.0.0',
|
||||
description: 'Live streaming control room with schedules and scene control',
|
||||
author: 'MetaBuilder',
|
||||
category: 'media',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const streamCastExamples: any = {}
|
||||
|
||||
export const DEFAULT_PACKAGES: Record<string, PackageSeedConfig> = {
|
||||
admin_dialog: {
|
||||
metadata: adminDialogMetadata,
|
||||
components: adminDialogComponents,
|
||||
examples: adminDialogExamples,
|
||||
},
|
||||
data_table: {
|
||||
metadata: dataTableMetadata,
|
||||
components: dataTableComponents,
|
||||
examples: dataTableExamples,
|
||||
},
|
||||
form_builder: {
|
||||
metadata: formBuilderMetadata,
|
||||
components: formBuilderComponents,
|
||||
examples: formBuilderExamples,
|
||||
},
|
||||
nav_menu: {
|
||||
metadata: navMenuMetadata,
|
||||
components: navMenuComponents,
|
||||
examples: navMenuExamples,
|
||||
},
|
||||
dashboard: {
|
||||
metadata: dashboardMetadata,
|
||||
components: dashboardComponents,
|
||||
examples: dashboardExamples,
|
||||
},
|
||||
notification_center: {
|
||||
metadata: notificationCenterMetadata,
|
||||
components: notificationCenterComponents,
|
||||
examples: notificationCenterExamples,
|
||||
},
|
||||
social_hub: {
|
||||
metadata: socialHubMetadata,
|
||||
components: socialHubComponents,
|
||||
examples: socialHubExamples,
|
||||
},
|
||||
codegen_studio: {
|
||||
metadata: codegenStudioMetadata,
|
||||
components: codegenStudioComponents,
|
||||
examples: codegenStudioExamples,
|
||||
},
|
||||
forum_forge: {
|
||||
metadata: forumForgeMetadata,
|
||||
components: forumForgeComponents,
|
||||
examples: forumForgeExamples,
|
||||
},
|
||||
arcade_lobby: {
|
||||
metadata: arcadeLobbyMetadata,
|
||||
components: arcadeLobbyComponents,
|
||||
examples: arcadeLobbyExamples,
|
||||
},
|
||||
stream_cast: {
|
||||
metadata: streamCastMetadata,
|
||||
components: streamCastComponents,
|
||||
examples: streamCastExamples,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PackageSeedConfig } from '../types'
|
||||
|
||||
const components: any[] = []
|
||||
const metadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'admin_dialog',
|
||||
name: 'Admin Dialog',
|
||||
version: '1.0.0',
|
||||
description: 'Admin dialog package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const examples: any = {}
|
||||
|
||||
export const adminDialog: PackageSeedConfig = {
|
||||
metadata,
|
||||
components,
|
||||
examples,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PackageSeedConfig } from '../types'
|
||||
|
||||
const components: any[] = []
|
||||
const metadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'data_table',
|
||||
name: 'Data Table',
|
||||
version: '1.0.0',
|
||||
description: 'Data table package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const examples: any = {}
|
||||
|
||||
export const dataTable: PackageSeedConfig = {
|
||||
metadata,
|
||||
components,
|
||||
examples,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PackageSeedConfig } from '../types'
|
||||
|
||||
const components: any[] = []
|
||||
const metadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'form_builder',
|
||||
name: 'Form Builder',
|
||||
version: '1.0.0',
|
||||
description: 'Form builder package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const examples: any = {}
|
||||
|
||||
export const formBuilder: PackageSeedConfig = {
|
||||
metadata,
|
||||
components,
|
||||
examples,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PackageSeedConfig } from '../types'
|
||||
|
||||
const components: any[] = []
|
||||
const metadata: PackageSeedConfig['metadata'] = {
|
||||
packageId: 'nav_menu',
|
||||
name: 'Navigation Menu',
|
||||
version: '1.0.0',
|
||||
description: 'Navigation menu package',
|
||||
author: 'MetaBuilder',
|
||||
category: 'ui',
|
||||
dependencies: [],
|
||||
exports: { components: [] },
|
||||
}
|
||||
const examples: any = {}
|
||||
|
||||
export const navMenu: PackageSeedConfig = {
|
||||
metadata,
|
||||
components,
|
||||
examples,
|
||||
}
|
||||
157
frontends/nextjs/src/types/dbal.d.ts
vendored
157
frontends/nextjs/src/types/dbal.d.ts
vendored
@@ -2,153 +2,14 @@
|
||||
* DBAL type stubs
|
||||
* These types are used when the full DBAL module is not available
|
||||
* The actual implementation lives in ../../dbal/development/src
|
||||
*
|
||||
* The declarations are split into focused modules to keep each file
|
||||
* under the large-file threshold and easier to maintain.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '@/dbal/development/src' {
|
||||
export interface DBALConfig {
|
||||
mode?: 'development' | 'production'
|
||||
adapter?: string
|
||||
auth?: any
|
||||
database?: {
|
||||
url?: string
|
||||
}
|
||||
security?: {
|
||||
sandbox?: 'strict' | 'permissive' | 'disabled'
|
||||
enableAuditLog?: boolean
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface DBALUser {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
username?: string
|
||||
level?: number
|
||||
role?: string
|
||||
tenantId?: string
|
||||
createdAt?: number | string | Date
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ListResult<T> {
|
||||
data: T[]
|
||||
total?: number
|
||||
}
|
||||
|
||||
export interface UsersAPI {
|
||||
list(): Promise<ListResult<DBALUser>>
|
||||
create(data: Partial<DBALUser>): Promise<DBALUser>
|
||||
update(id: string, data: Partial<DBALUser>): Promise<DBALUser>
|
||||
delete(id: string): Promise<boolean>
|
||||
}
|
||||
|
||||
export class DBALClient {
|
||||
users: UsersAPI
|
||||
constructor(config: DBALConfig)
|
||||
query<T>(sql: string, params?: unknown[]): Promise<T[]>
|
||||
execute(sql: string, params?: unknown[]): Promise<void>
|
||||
capabilities(): Promise<Record<string, boolean>>
|
||||
}
|
||||
|
||||
export class DBALError extends Error {
|
||||
code: DBALErrorCode
|
||||
message: string
|
||||
constructor(message: string, code: DBALErrorCode)
|
||||
}
|
||||
|
||||
export enum DBALErrorCode {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
CONNECTION_ERROR = 'CONNECTION_ERROR',
|
||||
QUERY_ERROR = 'QUERY_ERROR',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@/dbal/development/src/core/types' {
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
level: number
|
||||
tenantId: string
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@/dbal/development/src/core/tenant-context' {
|
||||
export interface TenantContext {
|
||||
tenantId: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export class InMemoryTenantManager {
|
||||
setCurrentTenant(tenantId: string): void
|
||||
getCurrentTenant(): string | null
|
||||
createTenant(id: string, metadata: Record<string, any>, ...args: any[]): Promise<void>
|
||||
getTenantContext(tenantId: string, userId?: string): Promise<TenantContext>
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@/dbal/development/src/core/kv-store' {
|
||||
import type { TenantContext } from '@/dbal/development/src/core/tenant-context'
|
||||
|
||||
export class InMemoryKVStore {
|
||||
get<T>(key: string, context?: TenantContext): Promise<T | null>
|
||||
set<T>(key: string, value: T, context?: TenantContext, ttl?: number): Promise<void>
|
||||
delete(key: string, context?: TenantContext): Promise<boolean>
|
||||
listAdd(key: string, items: any[], context?: TenantContext): Promise<void>
|
||||
listGet(key: string, context?: TenantContext, start?: number, end?: number): Promise<any[]>
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@/dbal/development/src/blob' {
|
||||
export interface BlobStorageConfig {
|
||||
type: 'filesystem' | 'memory' | 's3'
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
export interface BlobMetadata {
|
||||
contentType?: string
|
||||
size?: number
|
||||
lastModified?: Date
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BlobListItem {
|
||||
key: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BlobListResult {
|
||||
items: BlobListItem[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BlobStorage {
|
||||
upload(key: string, data: Buffer | string, metadata?: BlobMetadata): Promise<string>
|
||||
download(key: string): Promise<Buffer>
|
||||
delete(key: string): Promise<void>
|
||||
exists(key: string): Promise<boolean>
|
||||
list(options?: { prefix?: string }): Promise<BlobListResult>
|
||||
getMetadata(key: string): Promise<BlobMetadata | null>
|
||||
}
|
||||
|
||||
export function createBlobStorage(config: BlobStorageConfig): BlobStorage
|
||||
}
|
||||
|
||||
declare module '@/dbal/development/src/blob/tenant-aware-storage' {
|
||||
import type { BlobStorage, BlobMetadata, BlobListResult } from '@/dbal/development/src/blob'
|
||||
import type { InMemoryTenantManager } from '@/dbal/development/src/core/tenant-context'
|
||||
|
||||
export class TenantAwareBlobStorage implements BlobStorage {
|
||||
constructor(storage: BlobStorage, tenantManager: InMemoryTenantManager, ...args: any[])
|
||||
upload(key: string, data: Buffer | string, metadata?: BlobMetadata): Promise<string>
|
||||
download(key: string): Promise<Buffer>
|
||||
delete(key: string): Promise<void>
|
||||
exists(key: string): Promise<boolean>
|
||||
list(options?: { prefix?: string }): Promise<BlobListResult>
|
||||
getMetadata(key: string): Promise<BlobMetadata | null>
|
||||
}
|
||||
}
|
||||
/// <reference path="./dbal/core-config.d.ts" />
|
||||
/// <reference path="./dbal/core-types.d.ts" />
|
||||
/// <reference path="./dbal/tenant-context.d.ts" />
|
||||
/// <reference path="./dbal/kv-store.d.ts" />
|
||||
/// <reference path="./dbal/blob.d.ts" />
|
||||
/// <reference path="./dbal/tenant-aware-blob.d.ts" />
|
||||
|
||||
36
frontends/nextjs/src/types/dbal/blob.d.ts
vendored
Normal file
36
frontends/nextjs/src/types/dbal/blob.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '@/dbal/development/src/blob' {
|
||||
export interface BlobStorageConfig {
|
||||
type: 'filesystem' | 'memory' | 's3'
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
export interface BlobMetadata {
|
||||
contentType?: string
|
||||
size?: number
|
||||
lastModified?: Date
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BlobListItem {
|
||||
key: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BlobListResult {
|
||||
items: BlobListItem[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BlobStorage {
|
||||
upload(key: string, data: Buffer | string, metadata?: BlobMetadata): Promise<string>
|
||||
download(key: string): Promise<Buffer>
|
||||
delete(key: string): Promise<void>
|
||||
exists(key: string): Promise<boolean>
|
||||
list(options?: { prefix?: string }): Promise<BlobListResult>
|
||||
getMetadata(key: string): Promise<BlobMetadata | null>
|
||||
}
|
||||
|
||||
export function createBlobStorage(config: BlobStorageConfig): BlobStorage
|
||||
}
|
||||
66
frontends/nextjs/src/types/dbal/core-config.d.ts
vendored
Normal file
66
frontends/nextjs/src/types/dbal/core-config.d.ts
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Root DBAL module declarations.
|
||||
* Split from the monolithic dbal.d.ts to keep each set of exports contained.
|
||||
*/
|
||||
declare module '@/dbal/development/src' {
|
||||
export interface DBALConfig {
|
||||
mode?: 'development' | 'production'
|
||||
adapter?: string
|
||||
auth?: any
|
||||
database?: {
|
||||
url?: string
|
||||
}
|
||||
security?: {
|
||||
sandbox?: 'strict' | 'permissive' | 'disabled'
|
||||
enableAuditLog?: boolean
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface DBALUser {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
username?: string
|
||||
level?: number
|
||||
role?: string
|
||||
tenantId?: string
|
||||
createdAt?: number | string | Date
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ListResult<T> {
|
||||
data: T[]
|
||||
total?: number
|
||||
}
|
||||
|
||||
export interface UsersAPI {
|
||||
list(): Promise<ListResult<DBALUser>>
|
||||
create(data: Partial<DBALUser>): Promise<DBALUser>
|
||||
update(id: string, data: Partial<DBALUser>): Promise<DBALUser>
|
||||
delete(id: string): Promise<boolean>
|
||||
}
|
||||
|
||||
export class DBALClient {
|
||||
users: UsersAPI
|
||||
constructor(config: DBALConfig)
|
||||
query<T>(sql: string, params?: unknown[]): Promise<T[]>
|
||||
execute(sql: string, params?: unknown[]): Promise<void>
|
||||
capabilities(): Promise<Record<string, boolean>>
|
||||
}
|
||||
|
||||
export class DBALError extends Error {
|
||||
code: DBALErrorCode
|
||||
message: string
|
||||
constructor(message: string, code: DBALErrorCode)
|
||||
}
|
||||
|
||||
export enum DBALErrorCode {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
CONNECTION_ERROR = 'CONNECTION_ERROR',
|
||||
QUERY_ERROR = 'QUERY_ERROR',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
}
|
||||
}
|
||||
11
frontends/nextjs/src/types/dbal/core-types.d.ts
vendored
Normal file
11
frontends/nextjs/src/types/dbal/core-types.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '@/dbal/development/src/core/types' {
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
level: number
|
||||
tenantId: string
|
||||
}
|
||||
}
|
||||
13
frontends/nextjs/src/types/dbal/kv-store.d.ts
vendored
Normal file
13
frontends/nextjs/src/types/dbal/kv-store.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '@/dbal/development/src/core/kv-store' {
|
||||
import type { TenantContext } from '@/dbal/development/src/core/tenant-context'
|
||||
|
||||
export class InMemoryKVStore {
|
||||
get<T>(key: string, context?: TenantContext): Promise<T | null>
|
||||
set<T>(key: string, value: T, context?: TenantContext, ttl?: number): Promise<void>
|
||||
delete(key: string, context?: TenantContext): Promise<boolean>
|
||||
listAdd(key: string, items: any[], context?: TenantContext): Promise<void>
|
||||
listGet(key: string, context?: TenantContext, start?: number, end?: number): Promise<any[]>
|
||||
}
|
||||
}
|
||||
16
frontends/nextjs/src/types/dbal/tenant-aware-blob.d.ts
vendored
Normal file
16
frontends/nextjs/src/types/dbal/tenant-aware-blob.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '@/dbal/development/src/blob/tenant-aware-storage' {
|
||||
import type { BlobStorage, BlobMetadata, BlobListResult } from '@/dbal/development/src/blob'
|
||||
import type { InMemoryTenantManager } from '@/dbal/development/src/core/tenant-context'
|
||||
|
||||
export class TenantAwareBlobStorage implements BlobStorage {
|
||||
constructor(storage: BlobStorage, tenantManager: InMemoryTenantManager, ...args: any[])
|
||||
upload(key: string, data: Buffer | string, metadata?: BlobMetadata): Promise<string>
|
||||
download(key: string): Promise<Buffer>
|
||||
delete(key: string): Promise<void>
|
||||
exists(key: string): Promise<boolean>
|
||||
list(options?: { prefix?: string }): Promise<BlobListResult>
|
||||
getMetadata(key: string): Promise<BlobMetadata | null>
|
||||
}
|
||||
}
|
||||
15
frontends/nextjs/src/types/dbal/tenant-context.d.ts
vendored
Normal file
15
frontends/nextjs/src/types/dbal/tenant-context.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '@/dbal/development/src/core/tenant-context' {
|
||||
export interface TenantContext {
|
||||
tenantId: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export class InMemoryTenantManager {
|
||||
setCurrentTenant(tenantId: string): void
|
||||
getCurrentTenant(): string | null
|
||||
createTenant(id: string, metadata: Record<string, any>, ...args: any[]): Promise<void>
|
||||
getTenantContext(tenantId: string, userId?: string): Promise<TenantContext>
|
||||
}
|
||||
}
|
||||
@@ -320,8 +320,9 @@ DataConsistency ==
|
||||
|
||||
\* Package consistency: installed packages must be in installed or disabled state
|
||||
PackageConsistency ==
|
||||
\A t \in Tenants, p \in installedPackages[t]:
|
||||
packageStates[p] \in {"installed", "disabled", "installing"}
|
||||
\A t \in Tenants:
|
||||
\A p \in installedPackages[t]:
|
||||
packageStates[p] \in {"installed", "disabled", "installing"}
|
||||
|
||||
\* DBAL safety: no queries processed in error state
|
||||
DBALSafety ==
|
||||
|
||||
@@ -28,6 +28,10 @@ Scripts organized by function:
|
||||
- **`diagnose-workflows.sh`** - Diagnose workflow issues
|
||||
- **`capture-screenshot.ts`** - Capture UI screenshots
|
||||
|
||||
### Refactors & Analysis
|
||||
|
||||
- **`analysis/function_isolation_refactor.py`** - Build one-function-per-file refactor plans for TypeScript/C++ and generate TODO stub files with intentional errors for later cleanup.
|
||||
|
||||
### Project Management
|
||||
|
||||
- **`project-management/populate-kanban.py`** - Populate GitHub project board from TODO files
|
||||
|
||||
31
tools/analysis/code/large-typescript-refactor.md
Normal file
31
tools/analysis/code/large-typescript-refactor.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Large TypeScript file audit
|
||||
|
||||
This helper documents the process for converting oversized TypeScript/TSX files into a modular, lambda-per-file layout.
|
||||
|
||||
## Goal
|
||||
- Keep each file focused on a single exported lambda/component.
|
||||
- Extract helpers, constants, and view fragments into adjacent modules to lower per-file cognitive load.
|
||||
- Track remaining oversized files to schedule refactors.
|
||||
|
||||
## Usage
|
||||
1. From the repository root, run the analyzer:
|
||||
```bash
|
||||
npx tsx tools/analysis/code/list-large-typescript-files.ts --max-lines 150 --out tools/analysis/code/reports/large-ts-files.json
|
||||
```
|
||||
2. The report records every `.ts`/`.tsx` file above the threshold, their line counts, and a recommendation to break them into smaller modules.
|
||||
3. Use the sorted list to prioritize the longest files and migrate logic into dedicated helpers (one primary lambda per file).
|
||||
|
||||
## Output format
|
||||
The generated JSON includes:
|
||||
- `root`: scan starting directory.
|
||||
- `maxLines`: line threshold used for the run.
|
||||
- `ignored`: directories skipped (tune with `--ignore`).
|
||||
- `scanned`: count of `.ts`/`.tsx` files inspected.
|
||||
- `overLimit`: number of files exceeding the threshold.
|
||||
- `files`: array of file records with paths, line counts, and an extraction recommendation.
|
||||
|
||||
## Suggested refactor steps per file
|
||||
1. Identify the main exported lambda/component and keep it in place.
|
||||
2. Move derived data builders, parsing helpers, or rendering sub-sections into nearby modules (e.g., `./helpers/<name>.ts`).
|
||||
3. Re-export shared types from the original file when needed to avoid import churn.
|
||||
4. Keep UI-only fragments in `.tsx` leaf components; move business logic into `.ts` utilities when possible.
|
||||
32
tools/analysis/code/list-large-typescript-files.ts
Normal file
32
tools/analysis/code/list-large-typescript-files.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { parseArgs } from './list-large-typescript-files/args'
|
||||
import { walkFiles } from './list-large-typescript-files/file-scanner'
|
||||
import { writeSummary } from './list-large-typescript-files/write-summary'
|
||||
import type { Summary } from './list-large-typescript-files/types'
|
||||
|
||||
const buildSummary = async (): Promise<Summary> => {
|
||||
const options = parseArgs()
|
||||
const { reports, total } = await walkFiles(options)
|
||||
|
||||
return {
|
||||
root: options.root,
|
||||
maxLines: options.maxLines,
|
||||
ignored: Array.from(options.ignore).sort(),
|
||||
scanned: total,
|
||||
overLimit: reports.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
files: reports,
|
||||
outFile: options.outFile,
|
||||
}
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const summary = await buildSummary()
|
||||
await writeSummary(summary, summary.outFile)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to generate TypeScript file size report', error)
|
||||
process.exit(1)
|
||||
})
|
||||
52
tools/analysis/code/list-large-typescript-files/args.ts
Normal file
52
tools/analysis/code/list-large-typescript-files/args.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import path from 'path'
|
||||
|
||||
import { DEFAULT_IGNORE, Options, usage } from './types'
|
||||
|
||||
const extendIgnore = (options: Options, commaSeparated?: string) => {
|
||||
const extra = (commaSeparated ?? '')
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
extra.forEach((entry) => options.ignore.add(entry))
|
||||
}
|
||||
|
||||
export const parseArgs = (): Options => {
|
||||
const args = process.argv.slice(2)
|
||||
const options: Options = {
|
||||
root: process.cwd(),
|
||||
maxLines: 150,
|
||||
ignore: new Set(DEFAULT_IGNORE),
|
||||
}
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i]
|
||||
switch (arg) {
|
||||
case '--root':
|
||||
options.root = path.resolve(args[i + 1] ?? '.')
|
||||
i += 1
|
||||
break
|
||||
case '--max-lines':
|
||||
options.maxLines = Number(args[i + 1] ?? options.maxLines)
|
||||
i += 1
|
||||
break
|
||||
case '--out':
|
||||
options.outFile = args[i + 1]
|
||||
i += 1
|
||||
break
|
||||
case '--ignore':
|
||||
extendIgnore(options, args[i + 1])
|
||||
i += 1
|
||||
break
|
||||
case '--help':
|
||||
console.log(usage)
|
||||
process.exit(0)
|
||||
default:
|
||||
if (arg.startsWith('--')) {
|
||||
console.warn(`Unknown option: ${arg}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
import type { FileReport, Options } from './types'
|
||||
|
||||
const countLines = async (filePath: string): Promise<number> => {
|
||||
const content = await fs.readFile(filePath, 'utf8')
|
||||
return content.split(/\r?\n/).length
|
||||
}
|
||||
|
||||
const shouldSkip = (segment: string, ignore: Set<string>): boolean => ignore.has(segment)
|
||||
|
||||
export const walkFiles = async (options: Options): Promise<{ reports: FileReport[]; total: number }> => {
|
||||
const queue: string[] = [options.root]
|
||||
const reports: FileReport[] = []
|
||||
let total = 0
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.pop() as string
|
||||
const entries = await fs.readdir(current, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (shouldSkip(entry.name, options.ignore)) continue
|
||||
|
||||
const fullPath = path.join(current, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
queue.push(fullPath)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.tsx')) continue
|
||||
|
||||
total += 1
|
||||
const lines = await countLines(fullPath)
|
||||
if (lines > options.maxLines) {
|
||||
const relativePath = path.relative(options.root, fullPath)
|
||||
reports.push({
|
||||
path: relativePath,
|
||||
lines,
|
||||
recommendation: 'Split into focused modules; keep one primary lambda per file and extract helpers.',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { reports: reports.sort((a, b) => b.lines - a.lines), total }
|
||||
}
|
||||
39
tools/analysis/code/list-large-typescript-files/types.ts
Normal file
39
tools/analysis/code/list-large-typescript-files/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface Options {
|
||||
root: string
|
||||
maxLines: number
|
||||
ignore: Set<string>
|
||||
outFile?: string
|
||||
}
|
||||
|
||||
export interface FileReport {
|
||||
path: string
|
||||
lines: number
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
root: string
|
||||
maxLines: number
|
||||
ignored: string[]
|
||||
scanned: number
|
||||
overLimit: number
|
||||
timestamp: string
|
||||
files: FileReport[]
|
||||
outFile?: string
|
||||
}
|
||||
|
||||
export const DEFAULT_IGNORE = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'.next',
|
||||
'dist',
|
||||
'build',
|
||||
'coverage',
|
||||
'out',
|
||||
'tmp',
|
||||
'.turbo',
|
||||
]
|
||||
|
||||
export const usage = `Usage: tsx list-large-typescript-files.ts [--root <path>] [--max-lines <number>] [--out <path>] [--ignore <dirA,dirB>]
|
||||
|
||||
Scans the repository for .ts/.tsx files longer than the threshold and suggests splitting them into a modular, lambda-per-file structure.`
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
import type { Summary } from './types'
|
||||
|
||||
export const writeSummary = async (summary: Summary, destination?: string) => {
|
||||
if (!destination) {
|
||||
console.log(JSON.stringify(summary, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(destination), { recursive: true })
|
||||
await fs.writeFile(destination, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
||||
console.log(`Report written to ${destination}`)
|
||||
}
|
||||
521
tools/analysis/code/reports/large-ts-files.json
Normal file
521
tools/analysis/code/reports/large-ts-files.json
Normal file
@@ -0,0 +1,521 @@
|
||||
{
|
||||
"root": "/workspace/metabuilder",
|
||||
"maxLines": 150,
|
||||
"ignored": [
|
||||
".git",
|
||||
".next",
|
||||
".turbo",
|
||||
"build",
|
||||
"coverage",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"out",
|
||||
"tmp"
|
||||
],
|
||||
"scanned": 1407,
|
||||
"overLimit": 100,
|
||||
"timestamp": "2025-12-27T15:45:04.923Z",
|
||||
"files": [
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/packages/core/package-catalog.ts",
|
||||
"lines": 1170,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/github/GitHubActionsFetcher.tsx",
|
||||
"lines": 1070,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/editors/lua/LuaBlocksEditor.tsx",
|
||||
"lines": 1049,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/lua/snippets/lua-snippets-data.ts",
|
||||
"lines": 984,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/editors/lua/LuaEditor.tsx",
|
||||
"lines": 682,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/packages/tests/package-glue.test.ts",
|
||||
"lines": 620,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/package/PackageImportExport.tsx",
|
||||
"lines": 595,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/workflow/WorkflowEditor.tsx",
|
||||
"lines": 509,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/package/PackageManager.tsx",
|
||||
"lines": 492,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/rendering/page/page-definition-builder.ts",
|
||||
"lines": 484,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/component/ComponentHierarchyEditor.tsx",
|
||||
"lines": 479,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/db/database-admin/seed-default-data.ts",
|
||||
"lines": 472,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/schema/schema-utils.test.ts",
|
||||
"lines": 441,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/demos/DBALDemo.tsx",
|
||||
"lines": 429,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/blob/providers/filesystem-storage.ts",
|
||||
"lines": 411,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/navigation/Pagination.tsx",
|
||||
"lines": 407,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/workflow/engine/workflow-engine.test.ts",
|
||||
"lines": 389,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/navigation/Navigation.tsx",
|
||||
"lines": 372,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/blob/providers/s3-storage.ts",
|
||||
"lines": 362,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/lua/engine/core/lua-engine.test.ts",
|
||||
"lines": 358,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/rendering/tests/declarative-component-renderer.test.ts",
|
||||
"lines": 356,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx",
|
||||
"lines": 353,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/adapters/prisma-adapter.ts",
|
||||
"lines": 351,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/shared/tools/cpp-build-assistant.ts",
|
||||
"lines": 343,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/components/component-catalog.ts",
|
||||
"lines": 338,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level/levels/Level3.tsx",
|
||||
"lines": 336,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/UserManagement.tsx",
|
||||
"lines": 335,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/analysis/test/analyze-test-coverage.ts",
|
||||
"lines": 333,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/editors/schema/SchemaEditorLevel4.tsx",
|
||||
"lines": 331,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/css/CssClassManager.tsx",
|
||||
"lines": 328,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/demos/IRCWebchatDeclarative.tsx",
|
||||
"lines": 320,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/viewers/ModelListView.tsx",
|
||||
"lines": 319,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/nerd-mode-ide/core/NerdModeIDE.tsx",
|
||||
"lines": 318,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/css/CssClassBuilder.tsx",
|
||||
"lines": 316,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/dbal/core/client/dbal-integration.ts",
|
||||
"lines": 314,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/navigation/Sidebar.tsx",
|
||||
"lines": 311,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/schema/default-schema.ts",
|
||||
"lines": 309,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/core/foundation/kv-store.ts",
|
||||
"lines": 308,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/data/QuickGuide.tsx",
|
||||
"lines": 298,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/analysis/code/analyze-render-performance.ts",
|
||||
"lines": 295,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/editors/ThemeEditor.tsx",
|
||||
"lines": 295,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/PageRoutesManager.tsx",
|
||||
"lines": 291,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/component/ComponentConfigDialog.tsx",
|
||||
"lines": 291,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level/levels/Level5.tsx",
|
||||
"lines": 290,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/editors/lua/LuaSnippetLibrary.tsx",
|
||||
"lines": 286,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/data/GenericPage.tsx",
|
||||
"lines": 275,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/demos/IRCWebchat.tsx",
|
||||
"lines": 272,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/dialogs/AlertDialog.tsx",
|
||||
"lines": 270,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/hooks/ui/state/useAutoRefresh.test.ts",
|
||||
"lines": 269,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/molecules/overlay/DropdownMenu.tsx",
|
||||
"lines": 269,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/nerd-mode-ide/templates/template-configs.ts",
|
||||
"lines": 268,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/rendering/tests/page-renderer.test.ts",
|
||||
"lines": 266,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/index.ts",
|
||||
"lines": 264,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/blob/providers/tenant-aware-storage.ts",
|
||||
"lines": 261,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/examples/ContactForm.example.tsx",
|
||||
"lines": 259,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/adapters/acl-adapter.ts",
|
||||
"lines": 259,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/security/scanner/security-scanner.test.ts",
|
||||
"lines": 258,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/dialogs/Sheet.tsx",
|
||||
"lines": 256,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/database/DatabaseManager.tsx",
|
||||
"lines": 256,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/misc/metrics/enforce-size-limits.ts",
|
||||
"lines": 250,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/organisms/security/SecurityWarningDialog.tsx",
|
||||
"lines": 248,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/app/codegen/CodegenStudioClient.tsx",
|
||||
"lines": 240,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/demos/ScreenshotAnalyzer.tsx",
|
||||
"lines": 234,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/rendering/RenderComponent.tsx",
|
||||
"lines": 232,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/analysis/test/analyze-implementation-completeness.ts",
|
||||
"lines": 231,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/blob/providers/memory-storage.ts",
|
||||
"lines": 231,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/sonner.tsx",
|
||||
"lines": 228,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/managers/DropdownConfigManager.tsx",
|
||||
"lines": 227,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/auth/UnifiedLogin.tsx",
|
||||
"lines": 221,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/rendering/PropertyInspector.tsx",
|
||||
"lines": 218,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/db/core/index.ts",
|
||||
"lines": 217,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/core/foundation/types.ts",
|
||||
"lines": 217,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/detection/detect-stub-implementations.ts",
|
||||
"lines": 216,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level/levels/Level1.tsx",
|
||||
"lines": 213,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/data/Form.tsx",
|
||||
"lines": 211,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/rendering/FieldRenderer.tsx",
|
||||
"lines": 211,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/molecules/overlay/DropdownMenu.tsx",
|
||||
"lines": 208,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level5/tabs/PowerTransferTab.tsx",
|
||||
"lines": 208,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/generation/generate-stub-report.ts",
|
||||
"lines": 205,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/auth/GodCredentialsSettings.tsx",
|
||||
"lines": 204,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level/panels/ModeratorPanel.tsx",
|
||||
"lines": 202,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/theme/types/theme.d.ts",
|
||||
"lines": 201,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/hooks/data/useKV.test.ts",
|
||||
"lines": 197,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/editors/JsonEditor.tsx",
|
||||
"lines": 197,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/molecules/overlay/Dialog.tsx",
|
||||
"lines": 192,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level/levels/Level2.tsx",
|
||||
"lines": 191,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/molecules/overlay/Dialog.tsx",
|
||||
"lines": 189,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/viewers/AuditLogViewer.tsx",
|
||||
"lines": 189,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/core/entities/operations/system/package-operations.ts",
|
||||
"lines": 186,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/core/entities/operations/core/user-operations.ts",
|
||||
"lines": 186,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/security/functions/patterns/javascript-patterns.ts",
|
||||
"lines": 185,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/misc/data/SMTPConfigEditor.tsx",
|
||||
"lines": 185,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/hooks/useAuth.test.ts",
|
||||
"lines": 182,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/rendering/page/page-renderer.ts",
|
||||
"lines": 179,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/level4/Level4Tabs.tsx",
|
||||
"lines": 178,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "tools/quality/code/check-code-complexity.ts",
|
||||
"lines": 176,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/ui/organisms/data/Table.tsx",
|
||||
"lines": 175,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "dbal/development/src/bridges/websocket-bridge.ts",
|
||||
"lines": 169,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/lib/github/workflows/analysis/runs/analyze-workflow-runs.ts",
|
||||
"lines": 165,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
},
|
||||
{
|
||||
"path": "frontends/nextjs/src/components/rendering/Builder.tsx",
|
||||
"lines": 164,
|
||||
"recommendation": "Split into focused modules; keep one primary lambda per file and extract helpers."
|
||||
}
|
||||
],
|
||||
"outFile": "tools/analysis/code/reports/large-ts-files.json"
|
||||
}
|
||||
233
tools/analysis/function_isolation_refactor.py
Normal file
233
tools/analysis/function_isolation_refactor.py
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Function isolation refactor helper.
|
||||
|
||||
This script scans TypeScript and C++ sources to build a plan for splitting
|
||||
files into one-function-per-file layouts. It can also generate intentionally
|
||||
broken stubs with TODO markers so refactors remain gated on manual review.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import List, Sequence
|
||||
|
||||
IGNORED_DIRS = {".git", "node_modules", "dist", "build", "out", "coverage", "vendor", "venv"}
|
||||
TS_EXTENSIONS = {".ts", ".tsx"}
|
||||
CPP_EXTENSIONS = {".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".h", ".c"}
|
||||
|
||||
|
||||
def _matches_language(path: Path, languages: Sequence[str]) -> bool:
|
||||
suffix = path.suffix.lower()
|
||||
if "typescript" in languages and suffix in TS_EXTENSIONS:
|
||||
return True
|
||||
if "cpp" in languages and suffix in CPP_EXTENSIONS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionCandidate:
|
||||
name: str
|
||||
source_kind: str
|
||||
parent: str | None
|
||||
recommended_filename: str
|
||||
notes: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilePlan:
|
||||
path: str
|
||||
language: str
|
||||
functions: List[FunctionCandidate]
|
||||
classes: List[str]
|
||||
raw_function_hits: int
|
||||
|
||||
|
||||
def _read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
|
||||
def _detect_classes(content: str) -> list[str]:
|
||||
class_pattern = re.compile(r"\bclass\s+([A-Za-z_][A-Za-z0-9_]*)")
|
||||
return list({match.group(1) for match in class_pattern.finditer(content)})
|
||||
|
||||
|
||||
def _detect_ts_functions(content: str) -> list[str]:
|
||||
names: list[str] = []
|
||||
function_patterns = [
|
||||
re.compile(r"\bfunction\s+([A-Za-z_][A-Za-z0-9_]*)\s*\("),
|
||||
re.compile(r"\bconst\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>"),
|
||||
re.compile(r"\bexport\s+default\s+function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\("),
|
||||
]
|
||||
for pattern in function_patterns:
|
||||
names.extend(match.group(1) for match in pattern.finditer(content))
|
||||
return list(dict.fromkeys(names))
|
||||
|
||||
|
||||
def _detect_cpp_functions(content: str) -> list[str]:
|
||||
cpp_pattern = re.compile(
|
||||
r"^[\w:<>&\*\s]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;{]*\)\s*(?:const)?\s*\{",
|
||||
re.MULTILINE,
|
||||
)
|
||||
return list(dict.fromkeys(match.group(1) for match in cpp_pattern.finditer(content)))
|
||||
|
||||
|
||||
def _recommended_filename(name: str, existing: set[str], extension: str) -> str:
|
||||
candidate = f"{name}{extension}"
|
||||
counter = 1
|
||||
while candidate in existing:
|
||||
candidate = f"{name}_{counter}{extension}"
|
||||
counter += 1
|
||||
existing.add(candidate)
|
||||
return candidate
|
||||
|
||||
|
||||
def _build_plan_for_file(path: Path, root: Path) -> FilePlan:
|
||||
content = _read_text(path)
|
||||
language = "typescript" if path.suffix.lower() in TS_EXTENSIONS else "cpp"
|
||||
classes = _detect_classes(content)
|
||||
if language == "typescript":
|
||||
function_names = _detect_ts_functions(content)
|
||||
extension = path.suffix if path.suffix.lower() in TS_EXTENSIONS else ".ts"
|
||||
else:
|
||||
function_names = _detect_cpp_functions(content)
|
||||
extension = ".cpp" if path.suffix.lower() in {".cpp", ".cc", ".cxx"} else path.suffix
|
||||
|
||||
seen = set[str]()
|
||||
candidates = []
|
||||
for name in function_names:
|
||||
notes = []
|
||||
if classes:
|
||||
notes.append("Function is in a file with class definitions; confirm container-only usage.")
|
||||
recommended = _recommended_filename(name, seen, extension)
|
||||
candidates.append(
|
||||
FunctionCandidate(
|
||||
name=name,
|
||||
source_kind="free_function",
|
||||
parent=None,
|
||||
recommended_filename=recommended,
|
||||
notes=notes,
|
||||
)
|
||||
)
|
||||
|
||||
rel_path = str(path.relative_to(root))
|
||||
return FilePlan(
|
||||
path=rel_path,
|
||||
language=language,
|
||||
functions=candidates,
|
||||
classes=classes,
|
||||
raw_function_hits=len(function_names),
|
||||
)
|
||||
|
||||
|
||||
def collect_plan(root: Path, languages: Sequence[str]) -> list[FilePlan]:
|
||||
plans: list[FilePlan] = []
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if any(part in IGNORED_DIRS for part in path.parts):
|
||||
continue
|
||||
if not _matches_language(path, languages):
|
||||
continue
|
||||
plans.append(_build_plan_for_file(path, root))
|
||||
return plans
|
||||
|
||||
|
||||
def _write_plan(plans: Sequence[FilePlan], output: Path) -> None:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = [asdict(plan) for plan in plans]
|
||||
output.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _build_stub_contents(language: str, function: FunctionCandidate, origin: str) -> str:
|
||||
todo_header = (
|
||||
f"// TODO: migrate `{function.name}` from {origin} into a dedicated module.\n"
|
||||
"// INTENTIONAL_ERROR: stub needs manual completion before it can compile.\n"
|
||||
)
|
||||
if language == "typescript":
|
||||
header = f"export function {function.name}() " + "{\n"
|
||||
body = (
|
||||
header
|
||||
+ " // TODO: port implementation details.\n"
|
||||
+ " const intentionallyBroken = ; // missing value on purpose.\n"
|
||||
+ " throw new Error('TODO implement function logic');\n"
|
||||
+ "}\n"
|
||||
)
|
||||
return todo_header + body
|
||||
broken_signature = f"auto {function.name}() -> void " + "{\n"
|
||||
body = (
|
||||
broken_signature
|
||||
+ " // TODO: copy signature, includes, and behavior.\n"
|
||||
+ " int missingReturnValue; // INTENTIONAL_ERROR: unused/uninitialized.\n"
|
||||
+ "}\n"
|
||||
)
|
||||
return todo_header + body
|
||||
|
||||
|
||||
def _write_stub_file(base_dir: Path, origin: str, plan: FilePlan, function: FunctionCandidate, force: bool) -> None:
|
||||
destination = base_dir.joinpath(Path(origin).parent, function.recommended_filename)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if destination.exists() and not force:
|
||||
return
|
||||
destination.write_text(_build_stub_contents(plan.language, function, origin), encoding="utf-8")
|
||||
|
||||
|
||||
def generate_stubs(plans: Sequence[FilePlan], root: Path, force: bool) -> None:
|
||||
for plan in plans:
|
||||
for function in plan.functions:
|
||||
_write_stub_file(root, plan.path, plan, function, force)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Prepare one-function-per-file refactor plans.")
|
||||
parser.add_argument("root", nargs="?", default=Path.cwd(), type=Path, help="Root directory to scan.")
|
||||
parser.add_argument(
|
||||
"--languages",
|
||||
nargs="+",
|
||||
default=["typescript", "cpp"],
|
||||
choices=["typescript", "cpp"],
|
||||
help="Languages to include in the scan.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plan-output",
|
||||
default=Path("function_isolation_plan.json"),
|
||||
type=Path,
|
||||
help="Path to write the plan JSON report.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--generate-stubs",
|
||||
action="store_true",
|
||||
help="Create intentionally broken TODO stubs under the stub-root directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stub-root",
|
||||
default=Path("refactor_stubs"),
|
||||
type=Path,
|
||||
help="Directory to write stub files when --generate-stubs is provided.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite existing stubs if present.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
plans = collect_plan(args.root.resolve(), args.languages)
|
||||
_write_plan(plans, args.plan_output)
|
||||
if args.generate_stubs:
|
||||
generate_stubs(plans, args.stub_root, args.force)
|
||||
print(f"Refactor plan wrote {len(plans)} file entries to {args.plan_output}")
|
||||
if args.generate_stubs:
|
||||
print(f"Stub files emitted to {args.stub_root} (force={args.force})")
|
||||
else:
|
||||
print("No stub files generated; rerun with --generate-stubs to create TODO placeholders.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,159 +1,5 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
|
||||
function generateQualitySummary(): string {
|
||||
let markdown = '# Quality Metrics Summary\n\n'
|
||||
|
||||
const reportPath = 'quality-reports/'
|
||||
const sections = [
|
||||
{
|
||||
name: '🔍 Code Quality',
|
||||
files: ['code-quality-reports/code-quality-reports'],
|
||||
icon: '📊'
|
||||
},
|
||||
{
|
||||
name: '🧪 Test Coverage',
|
||||
files: ['coverage-reports/coverage-metrics.json'],
|
||||
icon: '✓'
|
||||
},
|
||||
{
|
||||
name: '🔐 Security',
|
||||
files: ['security-reports/security-report.json'],
|
||||
icon: '🛡️'
|
||||
},
|
||||
{
|
||||
name: '📚 Documentation',
|
||||
files: ['documentation-reports/jsdoc-report.json'],
|
||||
icon: '📖'
|
||||
},
|
||||
{
|
||||
name: '⚡ Performance',
|
||||
files: ['performance-reports/bundle-analysis.json'],
|
||||
icon: '🚀'
|
||||
},
|
||||
{
|
||||
name: '📦 Dependencies',
|
||||
files: ['dependency-reports/license-report.json'],
|
||||
icon: '📦'
|
||||
},
|
||||
{
|
||||
name: '🎯 Type Safety',
|
||||
files: ['type-reports/ts-strict-report.json'],
|
||||
icon: '✔️'
|
||||
}
|
||||
]
|
||||
|
||||
markdown += '## Overview\n\n'
|
||||
markdown += '| Metric | Status | Details |\n'
|
||||
markdown += '|--------|--------|----------|\n'
|
||||
|
||||
for (const section of sections) {
|
||||
let status = '⚠️ No data'
|
||||
let details = 'Report not available'
|
||||
|
||||
for (const file of section.files) {
|
||||
const fullPath = `${reportPath}${file}`
|
||||
if (existsSync(fullPath)) {
|
||||
try {
|
||||
const content = readFileSync(fullPath, 'utf8')
|
||||
const data = JSON.parse(content)
|
||||
|
||||
if (data.coverage) {
|
||||
status = data.coverage >= 80 ? '✅ Pass' : '⚠️ Warning'
|
||||
details = `${data.coverage}% coverage`
|
||||
} else if (data.totalIssues !== undefined) {
|
||||
status = data.critical === 0 ? '✅ Pass' : '❌ Issues'
|
||||
details = `${data.totalIssues} issues (${data.critical} critical)`
|
||||
} else if (data.averageCoverage) {
|
||||
status = data.averageCoverage >= 70 ? '✅ Good' : '⚠️ Needs work'
|
||||
details = `${data.averageCoverage.toFixed(1)}% documented`
|
||||
}
|
||||
break
|
||||
} catch (e) {
|
||||
// Continue to next file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markdown += `| ${section.icon} ${section.name} | ${status} | ${details} |\n`
|
||||
}
|
||||
|
||||
markdown += '\n## Detailed Metrics\n\n'
|
||||
|
||||
// Code Complexity
|
||||
markdown += '### Code Complexity\n\n'
|
||||
const complexityPath = `${reportPath}code-quality-reports/code-quality-reports/complexity-report.json`
|
||||
if (existsSync(complexityPath)) {
|
||||
try {
|
||||
const complexity = JSON.parse(readFileSync(complexityPath, 'utf8'))
|
||||
markdown += `- **Total files analyzed**: ${complexity.totalFilesAnalyzed}\n`
|
||||
markdown += `- **Average complexity**: ${complexity.avgMaxComplexity?.toFixed(2) || 'N/A'}\n`
|
||||
markdown += `- **Violations**: ${complexity.totalViolations || 0}\n\n`
|
||||
} catch (e) {
|
||||
markdown += 'No data available\n\n'
|
||||
}
|
||||
}
|
||||
|
||||
// Security Issues
|
||||
markdown += '### Security Scan Results\n\n'
|
||||
const securityPath = `${reportPath}security-reports/security-reports/security-report.json`
|
||||
if (existsSync(securityPath)) {
|
||||
try {
|
||||
const security = JSON.parse(readFileSync(securityPath, 'utf8'))
|
||||
markdown += `- **Critical Issues**: ${security.critical || 0} ❌\n`
|
||||
markdown += `- **High Severity**: ${security.high || 0} ⚠️\n`
|
||||
markdown += `- **Medium Severity**: ${security.medium || 0} ℹ️\n`
|
||||
markdown += `- **Total Issues**: ${security.totalIssues || 0}\n\n`
|
||||
} catch (e) {
|
||||
markdown += 'No data available\n\n'
|
||||
}
|
||||
}
|
||||
|
||||
// File Size Analysis
|
||||
markdown += '### File Size Metrics\n\n'
|
||||
const filesizePath = `${reportPath}size-reports/size-reports/file-sizes-report.json`
|
||||
if (existsSync(filesizePath)) {
|
||||
try {
|
||||
const filesize = JSON.parse(readFileSync(filesizePath, 'utf8'))
|
||||
markdown += `- **Files analyzed**: ${filesize.totalFilesAnalyzed}\n`
|
||||
markdown += `- **Violations**: ${filesize.totalViolations || 0}\n`
|
||||
if (filesize.largestFiles) {
|
||||
markdown += `- **Largest file**: ${filesize.largestFiles[0]?.file || 'N/A'} (${filesize.largestFiles[0]?.lines || 0} lines)\n`
|
||||
}
|
||||
markdown += '\n'
|
||||
} catch (e) {
|
||||
markdown += 'No data available\n\n'
|
||||
}
|
||||
}
|
||||
|
||||
// Performance Metrics
|
||||
markdown += '### Performance Budget\n\n'
|
||||
const perfPath = `${reportPath}performance-reports/performance-reports/bundle-analysis.json`
|
||||
if (existsSync(perfPath)) {
|
||||
try {
|
||||
const perf = JSON.parse(readFileSync(perfPath, 'utf8'))
|
||||
markdown += `- **Total bundle size**: ${perf.totalBundleSize?.mb || 'N/A'}MB\n`
|
||||
markdown += `- **Gzip size**: ${perf.gzipSize?.mb || 'N/A'}MB\n`
|
||||
markdown += `- **Status**: ${perf.budgetStatus?.status === 'ok' ? '✅' : '⚠️'}\n\n`
|
||||
} catch (e) {
|
||||
markdown += 'No data available\n\n'
|
||||
}
|
||||
}
|
||||
|
||||
markdown += '---\n\n'
|
||||
markdown += '## Recommendations\n\n'
|
||||
markdown += '- 🎯 Maintain test coverage above 80%\n'
|
||||
markdown += '- 📚 Add JSDoc comments to exported functions\n'
|
||||
markdown += '- 🔍 Address complexity warnings for better maintainability\n'
|
||||
markdown += '- ⚡ Monitor bundle size to prevent performance degradation\n'
|
||||
markdown += '- 🔐 Fix any security issues before merging\n'
|
||||
markdown += '- 📖 Keep documentation up to date\n\n'
|
||||
|
||||
markdown += `**Report generated**: ${new Date().toISOString()}\n`
|
||||
|
||||
return markdown
|
||||
}
|
||||
import { generateQualitySummary } from './generate-quality-summary/quality-summary'
|
||||
|
||||
console.log(generateQualitySummary())
|
||||
|
||||
80
tools/generation/generate-quality-summary/details.ts
Normal file
80
tools/generation/generate-quality-summary/details.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { BASE_REPORT_PATH } from './sections'
|
||||
import { readJsonFile } from './report-reader'
|
||||
|
||||
const buildCodeComplexity = (): string => {
|
||||
const complexityPath = `${BASE_REPORT_PATH}code-quality-reports/code-quality-reports/complexity-report.json`
|
||||
const complexity = readJsonFile<any>(complexityPath)
|
||||
|
||||
if (!complexity) return 'No data available\n\n'
|
||||
|
||||
return [
|
||||
`- **Total files analyzed**: ${complexity.totalFilesAnalyzed}`,
|
||||
`- **Average complexity**: ${complexity.avgMaxComplexity?.toFixed(2) || 'N/A'}`,
|
||||
`- **Violations**: ${complexity.totalViolations || 0}`,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const buildSecurity = (): string => {
|
||||
const securityPath = `${BASE_REPORT_PATH}security-reports/security-reports/security-report.json`
|
||||
const security = readJsonFile<any>(securityPath)
|
||||
|
||||
if (!security) return 'No data available\n\n'
|
||||
|
||||
return [
|
||||
`- **Critical Issues**: ${security.critical || 0} ❌`,
|
||||
`- **High Severity**: ${security.high || 0} ⚠️`,
|
||||
`- **Medium Severity**: ${security.medium || 0} ℹ️`,
|
||||
`- **Total Issues**: ${security.totalIssues || 0}`,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const buildFileSizeMetrics = (): string => {
|
||||
const filesizePath = `${BASE_REPORT_PATH}size-reports/size-reports/file-sizes-report.json`
|
||||
const filesize = readJsonFile<any>(filesizePath)
|
||||
|
||||
if (!filesize) return 'No data available\n\n'
|
||||
|
||||
const largest = filesize.largestFiles?.[0]
|
||||
const largestLabel = largest ? `${largest.file} (${largest.lines || 0} lines)` : 'N/A'
|
||||
|
||||
return [
|
||||
`- **Files analyzed**: ${filesize.totalFilesAnalyzed}`,
|
||||
`- **Violations**: ${filesize.totalViolations || 0}`,
|
||||
`- **Largest file**: ${largestLabel}`,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const buildPerformance = (): string => {
|
||||
const perfPath = `${BASE_REPORT_PATH}performance-reports/performance-reports/bundle-analysis.json`
|
||||
const perf = readJsonFile<any>(perfPath)
|
||||
|
||||
if (!perf) return 'No data available\n\n'
|
||||
|
||||
const status = perf.budgetStatus?.status === 'ok' ? '✅' : '⚠️'
|
||||
|
||||
return [
|
||||
`- **Total bundle size**: ${perf.totalBundleSize?.mb || 'N/A'}MB`,
|
||||
`- **Gzip size**: ${perf.gzipSize?.mb || 'N/A'}MB`,
|
||||
`- **Status**: ${status}`,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export const buildDetailedMetrics = (): string => {
|
||||
let markdown = '### Code Complexity\n\n'
|
||||
markdown += `${buildCodeComplexity()}\n`
|
||||
|
||||
markdown += '### Security Scan Results\n\n'
|
||||
markdown += `${buildSecurity()}\n`
|
||||
|
||||
markdown += '### File Size Metrics\n\n'
|
||||
markdown += `${buildFileSizeMetrics()}\n`
|
||||
|
||||
markdown += '### Performance Budget\n\n'
|
||||
markdown += `${buildPerformance()}\n`
|
||||
|
||||
return markdown
|
||||
}
|
||||
52
tools/generation/generate-quality-summary/overview.ts
Normal file
52
tools/generation/generate-quality-summary/overview.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { BASE_REPORT_PATH, QUALITY_SECTIONS, SectionDefinition } from './sections'
|
||||
import { readJsonFile } from './report-reader'
|
||||
|
||||
type OverviewStatus = {
|
||||
status: string
|
||||
details: string
|
||||
}
|
||||
|
||||
const selectDetails = (data: any): OverviewStatus | undefined => {
|
||||
if (data?.coverage !== undefined) {
|
||||
return {
|
||||
status: data.coverage >= 80 ? '✅ Pass' : '⚠️ Warning',
|
||||
details: `${data.coverage}% coverage`,
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.totalIssues !== undefined) {
|
||||
return {
|
||||
status: data.critical === 0 ? '✅ Pass' : '❌ Issues',
|
||||
details: `${data.totalIssues} issues (${data.critical} critical)`,
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.averageCoverage !== undefined) {
|
||||
return {
|
||||
status: data.averageCoverage >= 70 ? '✅ Good' : '⚠️ Needs work',
|
||||
details: `${data.averageCoverage.toFixed(1)}% documented`,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const evaluateSection = (section: SectionDefinition): OverviewStatus => {
|
||||
for (const file of section.files) {
|
||||
const fullPath = `${BASE_REPORT_PATH}${file}`
|
||||
const data = readJsonFile(fullPath)
|
||||
const chosen = selectDetails(data)
|
||||
if (chosen) return chosen
|
||||
}
|
||||
|
||||
return { status: '⚠️ No data', details: 'Report not available' }
|
||||
}
|
||||
|
||||
export const buildOverviewTable = (): string => {
|
||||
const rows = QUALITY_SECTIONS.map((section) => {
|
||||
const { status, details } = evaluateSection(section)
|
||||
return `| ${section.icon} ${section.name} | ${status} | ${details} |\n`
|
||||
})
|
||||
|
||||
return rows.join('')
|
||||
}
|
||||
34
tools/generation/generate-quality-summary/quality-summary.ts
Normal file
34
tools/generation/generate-quality-summary/quality-summary.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { buildDetailedMetrics } from './details'
|
||||
import { buildOverviewTable } from './overview'
|
||||
|
||||
const buildRecommendations = () => {
|
||||
return [
|
||||
'- 🎯 Maintain test coverage above 80%',
|
||||
'- 📚 Add JSDoc comments to exported functions',
|
||||
'- 🔍 Address complexity warnings for better maintainability',
|
||||
'- ⚡ Monitor bundle size to prevent performance degradation',
|
||||
'- 🔐 Fix any security issues before merging',
|
||||
'- 📖 Keep documentation up to date',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export const generateQualitySummary = (): string => {
|
||||
let markdown = '# Quality Metrics Summary\n\n'
|
||||
|
||||
markdown += '## Overview\n\n'
|
||||
markdown += '| Metric | Status | Details |\n'
|
||||
markdown += '|--------|--------|----------|\n'
|
||||
markdown += buildOverviewTable()
|
||||
|
||||
markdown += '\n## Detailed Metrics\n\n'
|
||||
markdown += buildDetailedMetrics()
|
||||
|
||||
markdown += '---\n\n'
|
||||
markdown += '## Recommendations\n\n'
|
||||
markdown += `${buildRecommendations()}\n`
|
||||
|
||||
markdown += `**Report generated**: ${new Date().toISOString()}\n`
|
||||
|
||||
return markdown
|
||||
}
|
||||
12
tools/generation/generate-quality-summary/report-reader.ts
Normal file
12
tools/generation/generate-quality-summary/report-reader.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
|
||||
export const readJsonFile = <T = unknown>(fullPath: string): T | undefined => {
|
||||
if (!existsSync(fullPath)) return undefined
|
||||
|
||||
try {
|
||||
return JSON.parse(readFileSync(fullPath, 'utf8')) as T
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON report at ${fullPath}:`, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
45
tools/generation/generate-quality-summary/sections.ts
Normal file
45
tools/generation/generate-quality-summary/sections.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export interface SectionDefinition {
|
||||
name: string
|
||||
files: string[]
|
||||
icon: string
|
||||
}
|
||||
|
||||
export const BASE_REPORT_PATH = 'quality-reports/'
|
||||
|
||||
export const QUALITY_SECTIONS: SectionDefinition[] = [
|
||||
{
|
||||
name: '🔍 Code Quality',
|
||||
files: ['code-quality-reports/code-quality-reports'],
|
||||
icon: '📊',
|
||||
},
|
||||
{
|
||||
name: '🧪 Test Coverage',
|
||||
files: ['coverage-reports/coverage-metrics.json'],
|
||||
icon: '✓',
|
||||
},
|
||||
{
|
||||
name: '🔐 Security',
|
||||
files: ['security-reports/security-report.json'],
|
||||
icon: '🛡️',
|
||||
},
|
||||
{
|
||||
name: '📚 Documentation',
|
||||
files: ['documentation-reports/jsdoc-report.json'],
|
||||
icon: '📖',
|
||||
},
|
||||
{
|
||||
name: '⚡ Performance',
|
||||
files: ['performance-reports/bundle-analysis.json'],
|
||||
icon: '🚀',
|
||||
},
|
||||
{
|
||||
name: '📦 Dependencies',
|
||||
files: ['dependency-reports/license-report.json'],
|
||||
icon: '📦',
|
||||
},
|
||||
{
|
||||
name: '🎯 Type Safety',
|
||||
files: ['type-reports/ts-strict-report.json'],
|
||||
icon: '✔️',
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user