mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat(validation): enhance entity validation with new fields and improved checks
- Updated page validation to include new properties such as path, componentTree, requiresAuth, and more. - Enhanced session validation to check for bigint timestamps and added checks for ipAddress and userAgent. - Improved user validation by adding checks for profilePicture, bio, createdAt, and tenantId. - Refactored workflow validation to ensure nodes and edges are valid JSON strings and added checks for createdAt and updatedAt. - Introduced isValidJsonString utility for validating JSON strings. - Added seedUsers function to populate default users in the database. - Removed unused generateComponentTree stub and replaced it with a full implementation for rendering Lua UI components. - Updated useLevelRouting and useResolvedUser hooks to support new permission levels and user state management. - Enhanced useRestApi hook to support request cancellation with AbortSignal.
This commit is contained in:
@@ -1,23 +1,28 @@
|
||||
/**
|
||||
* @file component-operations.ts
|
||||
* @description ComponentHierarchy entity CRUD operations for DBAL client
|
||||
* @description ComponentNode entity CRUD operations for DBAL client
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { DBALAdapter } from '../../../../adapters/adapter'
|
||||
import type { ComponentHierarchy, ListResult, PageView } from '../../../foundation/types'
|
||||
import type {
|
||||
ComponentNode,
|
||||
CreateComponentNodeInput,
|
||||
ListResult,
|
||||
PageConfig,
|
||||
UpdateComponentNodeInput,
|
||||
} from '../../../foundation/types'
|
||||
import { DBALError } from '../../../foundation/errors'
|
||||
import { validateComponentHierarchyCreate, validateComponentHierarchyUpdate, validateId } from '../../../foundation/validation'
|
||||
|
||||
export interface ComponentOperations {
|
||||
create: (data: ComponentCreatePayload) => Promise<ComponentHierarchy>
|
||||
read: (id: string) => Promise<ComponentHierarchy | null>
|
||||
update: (id: string, data: Partial<ComponentHierarchy>) => Promise<ComponentHierarchy>
|
||||
export interface ComponentNodeOperations {
|
||||
create: (data: CreateComponentNodeInput) => Promise<ComponentNode>
|
||||
read: (id: string) => Promise<ComponentNode | null>
|
||||
update: (id: string, data: UpdateComponentNodeInput) => Promise<ComponentNode>
|
||||
delete: (id: string) => Promise<boolean>
|
||||
getTree: (pageId: string) => Promise<ComponentHierarchy[]>
|
||||
getTree: (pageId: string) => Promise<ComponentNode[]>
|
||||
}
|
||||
|
||||
type ComponentCreatePayload = Omit<ComponentHierarchy, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'> & { tenantId?: string }
|
||||
|
||||
const assertValidId = (id: string) => {
|
||||
const errors = validateId(id)
|
||||
if (errors.length > 0) {
|
||||
@@ -25,21 +30,21 @@ const assertValidId = (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const assertValidCreate = (data: ComponentCreatePayload) => {
|
||||
const assertValidCreate = (data: CreateComponentNodeInput) => {
|
||||
const errors = validateComponentHierarchyCreate(data)
|
||||
if (errors.length > 0) {
|
||||
throw DBALError.validationError('Invalid component data', errors.map(error => ({ field: 'component', error })))
|
||||
}
|
||||
}
|
||||
|
||||
const assertValidUpdate = (data: Partial<ComponentHierarchy>) => {
|
||||
const assertValidUpdate = (data: UpdateComponentNodeInput) => {
|
||||
const errors = validateComponentHierarchyUpdate(data)
|
||||
if (errors.length > 0) {
|
||||
throw DBALError.validationError('Invalid component update data', errors.map(error => ({ field: 'component', error })))
|
||||
}
|
||||
}
|
||||
|
||||
const resolveTenantId = (configuredTenantId?: string, data?: Partial<ComponentHierarchy>): string | null => {
|
||||
const resolveTenantId = (configuredTenantId?: string, data?: { tenantId?: string | null }): string | null => {
|
||||
if (configuredTenantId && configuredTenantId.length > 0) return configuredTenantId
|
||||
const candidate = data?.tenantId
|
||||
if (typeof candidate === 'string' && candidate.length > 0) return candidate
|
||||
@@ -47,22 +52,31 @@ const resolveTenantId = (configuredTenantId?: string, data?: Partial<ComponentHi
|
||||
}
|
||||
|
||||
const assertPageTenant = async (adapter: DBALAdapter, tenantId: string, pageId: string) => {
|
||||
const page = await adapter.findFirst('PageView', { id: pageId, tenantId }) as PageView | null
|
||||
const page = await adapter.findFirst('PageConfig', { id: pageId, tenantId }) as PageConfig | null
|
||||
if (!page) {
|
||||
throw DBALError.notFound(`Page not found: ${pageId}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const createComponentOperations = (adapter: DBALAdapter, tenantId?: string): ComponentOperations => ({
|
||||
const withComponentDefaults = (data: CreateComponentNodeInput): ComponentNode => ({
|
||||
id: data.id ?? randomUUID(),
|
||||
type: data.type,
|
||||
parentId: data.parentId ?? null,
|
||||
childIds: data.childIds,
|
||||
order: data.order,
|
||||
pageId: data.pageId,
|
||||
})
|
||||
|
||||
export const createComponentNodeOperations = (adapter: DBALAdapter, tenantId?: string): ComponentNodeOperations => ({
|
||||
create: async data => {
|
||||
const resolvedTenantId = resolveTenantId(tenantId, data)
|
||||
const resolvedTenantId = resolveTenantId(tenantId)
|
||||
if (!resolvedTenantId) {
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidCreate(data)
|
||||
await assertPageTenant(adapter, resolvedTenantId, data.pageId)
|
||||
const payload = { ...data, tenantId: resolvedTenantId }
|
||||
return adapter.create('ComponentHierarchy', payload) as Promise<ComponentHierarchy>
|
||||
const payload = withComponentDefaults(data)
|
||||
return adapter.create('ComponentNode', payload) as Promise<ComponentNode>
|
||||
},
|
||||
read: async id => {
|
||||
const resolvedTenantId = resolveTenantId(tenantId)
|
||||
@@ -70,30 +84,29 @@ export const createComponentOperations = (adapter: DBALAdapter, tenantId?: strin
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidId(id)
|
||||
const result = await adapter.findFirst('ComponentHierarchy', { id, tenantId: resolvedTenantId }) as ComponentHierarchy | null
|
||||
const result = await adapter.findFirst('ComponentNode', { id }) as ComponentNode | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Component not found: ${id}`)
|
||||
}
|
||||
await assertPageTenant(adapter, resolvedTenantId, result.pageId)
|
||||
return result
|
||||
},
|
||||
update: async (id, data) => {
|
||||
if (data.tenantId !== undefined) {
|
||||
throw DBALError.validationError('Tenant ID cannot be updated', [{ field: 'tenantId', error: 'tenantId is immutable' }])
|
||||
}
|
||||
const resolvedTenantId = resolveTenantId(tenantId)
|
||||
if (!resolvedTenantId) {
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidId(id)
|
||||
assertValidUpdate(data)
|
||||
if (data.pageId) {
|
||||
await assertPageTenant(adapter, resolvedTenantId, data.pageId)
|
||||
}
|
||||
const existing = await adapter.findFirst('ComponentHierarchy', { id, tenantId: resolvedTenantId }) as ComponentHierarchy | null
|
||||
const existing = await adapter.findFirst('ComponentNode', { id }) as ComponentNode | null
|
||||
if (!existing) {
|
||||
throw DBALError.notFound(`Component not found: ${id}`)
|
||||
}
|
||||
return adapter.update('ComponentHierarchy', id, data) as Promise<ComponentHierarchy>
|
||||
await assertPageTenant(adapter, resolvedTenantId, existing.pageId)
|
||||
if (data.pageId) {
|
||||
await assertPageTenant(adapter, resolvedTenantId, data.pageId)
|
||||
}
|
||||
return adapter.update('ComponentNode', id, data) as Promise<ComponentNode>
|
||||
},
|
||||
delete: async id => {
|
||||
const resolvedTenantId = resolveTenantId(tenantId)
|
||||
@@ -101,11 +114,12 @@ export const createComponentOperations = (adapter: DBALAdapter, tenantId?: strin
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidId(id)
|
||||
const existing = await adapter.findFirst('ComponentHierarchy', { id, tenantId: resolvedTenantId }) as ComponentHierarchy | null
|
||||
const existing = await adapter.findFirst('ComponentNode', { id }) as ComponentNode | null
|
||||
if (!existing) {
|
||||
throw DBALError.notFound(`Component not found: ${id}`)
|
||||
}
|
||||
const result = await adapter.delete('ComponentHierarchy', id)
|
||||
await assertPageTenant(adapter, resolvedTenantId, existing.pageId)
|
||||
const result = await adapter.delete('ComponentNode', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Component not found: ${id}`)
|
||||
}
|
||||
@@ -118,10 +132,12 @@ export const createComponentOperations = (adapter: DBALAdapter, tenantId?: strin
|
||||
}
|
||||
assertValidId(pageId)
|
||||
await assertPageTenant(adapter, resolvedTenantId, pageId)
|
||||
const result = await adapter.list('ComponentHierarchy', {
|
||||
filter: { pageId, tenantId: resolvedTenantId },
|
||||
const result = await adapter.list('ComponentNode', {
|
||||
filter: { pageId },
|
||||
sort: { order: 'asc' },
|
||||
}) as ListResult<ComponentHierarchy>
|
||||
}) as ListResult<ComponentNode>
|
||||
return result.data
|
||||
},
|
||||
})
|
||||
|
||||
export const createComponentOperations = createComponentNodeOperations
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { DBALAdapter } from '../../../../../adapters/adapter'
|
||||
import type { Package } from '../../../../foundation/types'
|
||||
import type { InstalledPackage } from '../../../../foundation/types'
|
||||
import { DBALError } from '../../../../foundation/errors'
|
||||
import { validatePackageCreate, validatePackageUpdate } from '../../../../foundation/validation'
|
||||
|
||||
export const createManyPackages = async (
|
||||
export const createManyInstalledPackages = async (
|
||||
adapter: DBALAdapter,
|
||||
data: Array<Omit<Package, 'id' | 'createdAt' | 'updatedAt'>>,
|
||||
data: InstalledPackage[],
|
||||
): Promise<number> => {
|
||||
if (!data || data.length === 0) {
|
||||
return 0
|
||||
@@ -15,23 +15,23 @@ export const createManyPackages = async (
|
||||
validatePackageCreate(item).map(error => ({ field: `packages[${index}]`, error })),
|
||||
)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package batch', validationErrors)
|
||||
throw DBALError.validationError('Invalid installed package batch', validationErrors)
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.createMany('Package', data as Record<string, unknown>[])
|
||||
return adapter.createMany('InstalledPackage', data as Record<string, unknown>[])
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
throw DBALError.conflict('Installed package already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateManyPackages = async (
|
||||
export const updateManyInstalledPackages = async (
|
||||
adapter: DBALAdapter,
|
||||
filter: Record<string, unknown>,
|
||||
data: Partial<Package>,
|
||||
data: Partial<InstalledPackage>,
|
||||
): Promise<number> => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
throw DBALError.validationError('Bulk update requires a filter', [
|
||||
@@ -47,25 +47,31 @@ export const updateManyPackages = async (
|
||||
|
||||
const validationErrors = validatePackageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package update data', validationErrors.map(error => ({ field: 'package', error })))
|
||||
throw DBALError.validationError(
|
||||
'Invalid installed package update data',
|
||||
validationErrors.map(error => ({ field: 'package', error })),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.updateMany('Package', filter, data as Record<string, unknown>)
|
||||
return adapter.updateMany('InstalledPackage', filter, data as Record<string, unknown>)
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
throw DBALError.conflict('Installed package already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteManyPackages = async (adapter: DBALAdapter, filter: Record<string, unknown>): Promise<number> => {
|
||||
export const deleteManyInstalledPackages = async (
|
||||
adapter: DBALAdapter,
|
||||
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 adapter.deleteMany('Package', filter)
|
||||
return adapter.deleteMany('InstalledPackage', filter)
|
||||
}
|
||||
|
||||
@@ -1,61 +1,70 @@
|
||||
import type { DBALAdapter } from '../../../../../adapters/adapter'
|
||||
import type { Package } from '../../../../foundation/types'
|
||||
import type { InstalledPackage } from '../../../../foundation/types'
|
||||
import { DBALError } from '../../../../foundation/errors'
|
||||
import { validatePackageCreate, validatePackageUpdate, validateId } from '../../../../foundation/validation'
|
||||
|
||||
export const createPackage = async (
|
||||
export const createInstalledPackage = async (
|
||||
adapter: DBALAdapter,
|
||||
data: Omit<Package, 'id' | 'createdAt' | 'updatedAt'>,
|
||||
): Promise<Package> => {
|
||||
data: InstalledPackage,
|
||||
): Promise<InstalledPackage> => {
|
||||
const validationErrors = validatePackageCreate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package data', validationErrors.map(error => ({ field: 'package', error })))
|
||||
throw DBALError.validationError(
|
||||
'Invalid installed package data',
|
||||
validationErrors.map(error => ({ field: 'package', error })),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.create('Package', data) as Promise<Package>
|
||||
return adapter.create('InstalledPackage', data) as Promise<InstalledPackage>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict(`Package ${data.name}@${data.version} already exists`)
|
||||
throw DBALError.conflict(`Installed package ${data.packageId} already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updatePackage = async (
|
||||
export const updateInstalledPackage = async (
|
||||
adapter: DBALAdapter,
|
||||
id: string,
|
||||
data: Partial<Package>,
|
||||
): Promise<Package> => {
|
||||
const idErrors = validateId(id)
|
||||
packageId: string,
|
||||
data: Partial<InstalledPackage>,
|
||||
): Promise<InstalledPackage> => {
|
||||
const idErrors = validateId(packageId)
|
||||
if (idErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package ID', idErrors.map(error => ({ field: 'id', error })))
|
||||
throw DBALError.validationError('Invalid package ID', idErrors.map(error => ({ field: 'packageId', error })))
|
||||
}
|
||||
|
||||
const validationErrors = validatePackageUpdate(data)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package update data', validationErrors.map(error => ({ field: 'package', error })))
|
||||
throw DBALError.validationError(
|
||||
'Invalid installed package update data',
|
||||
validationErrors.map(error => ({ field: 'package', error })),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.update('Package', id, data) as Promise<Package>
|
||||
return adapter.update('InstalledPackage', packageId, data) as Promise<InstalledPackage>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
throw DBALError.conflict('Package name+version already exists')
|
||||
throw DBALError.conflict('Installed package already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deletePackage = async (adapter: DBALAdapter, id: string): Promise<boolean> => {
|
||||
const validationErrors = validateId(id)
|
||||
export const deleteInstalledPackage = async (
|
||||
adapter: DBALAdapter,
|
||||
packageId: string,
|
||||
): Promise<boolean> => {
|
||||
const validationErrors = validateId(packageId)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'id', error })))
|
||||
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'packageId', error })))
|
||||
}
|
||||
|
||||
const result = await adapter.delete('Package', id)
|
||||
const result = await adapter.delete('InstalledPackage', packageId)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Package not found: ${id}`)
|
||||
throw DBALError.notFound(`Installed package not found: ${packageId}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DBALAdapter } from '../../../../../adapters/adapter'
|
||||
import type { Package } from '../../../../foundation/types'
|
||||
import { createPackage } from './mutations'
|
||||
import type { InstalledPackage } from '../../../../foundation/types'
|
||||
import { createInstalledPackage } from './mutations'
|
||||
|
||||
export const publishPackage = (
|
||||
adapter: DBALAdapter,
|
||||
data: Omit<Package, 'id' | 'createdAt' | 'updatedAt'>,
|
||||
): Promise<Package> => {
|
||||
return createPackage(adapter, data)
|
||||
data: InstalledPackage,
|
||||
): Promise<InstalledPackage> => {
|
||||
return createInstalledPackage(adapter, data)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import type { DBALAdapter } from '../../../../../adapters/adapter'
|
||||
import type { Package, ListOptions, ListResult } from '../../../../foundation/types'
|
||||
import type { InstalledPackage, ListOptions, ListResult } from '../../../../foundation/types'
|
||||
import { DBALError } from '../../../../foundation/errors'
|
||||
import { validateId } from '../../../../foundation/validation'
|
||||
|
||||
export const readPackage = async (adapter: DBALAdapter, id: string): Promise<Package | null> => {
|
||||
const validationErrors = validateId(id)
|
||||
export const readInstalledPackage = async (
|
||||
adapter: DBALAdapter,
|
||||
packageId: string,
|
||||
): Promise<InstalledPackage | null> => {
|
||||
const validationErrors = validateId(packageId)
|
||||
if (validationErrors.length > 0) {
|
||||
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'id', error })))
|
||||
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'packageId', error })))
|
||||
}
|
||||
|
||||
const result = await adapter.read('Package', id) as Package | null
|
||||
const result = await adapter.read('InstalledPackage', packageId) as InstalledPackage | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Package not found: ${id}`)
|
||||
throw DBALError.notFound(`Installed package not found: ${packageId}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const listPackages = (adapter: DBALAdapter, options?: ListOptions): Promise<ListResult<Package>> => {
|
||||
return adapter.list('Package', options) as Promise<ListResult<Package>>
|
||||
export const listInstalledPackages = (
|
||||
adapter: DBALAdapter,
|
||||
options?: ListOptions,
|
||||
): Promise<ListResult<InstalledPackage>> => {
|
||||
return adapter.list('InstalledPackage', options) as Promise<ListResult<InstalledPackage>>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DBALAdapter } from '../../../../../adapters/adapter'
|
||||
import { deletePackage } from './mutations'
|
||||
import { deleteInstalledPackage } from './mutations'
|
||||
|
||||
export const unpublishPackage = (adapter: DBALAdapter, id: string): Promise<boolean> => {
|
||||
return deletePackage(adapter, id)
|
||||
return deleteInstalledPackage(adapter, id)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Package } from '../../../../foundation/types'
|
||||
import type { InstalledPackage } from '../../../../foundation/types'
|
||||
import { validatePackageCreate } from '../../../../foundation/validation'
|
||||
|
||||
export const validatePackage = (data: Partial<Package>): string[] => {
|
||||
export const validatePackage = (data: Partial<InstalledPackage>): string[] => {
|
||||
return validatePackageCreate(data)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
/**
|
||||
* @file page-operations.ts
|
||||
* @description PageView entity CRUD operations for DBAL client
|
||||
* @description PageConfig entity CRUD operations for DBAL client
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { DBALAdapter } from '../../../../adapters/adapter'
|
||||
import type { ListOptions, ListResult, PageView } from '../../../foundation/types'
|
||||
import type { CreatePageInput, ListOptions, ListResult, PageConfig, UpdatePageInput } from '../../../foundation/types'
|
||||
import { DBALError } from '../../../foundation/errors'
|
||||
import { isValidSlug, validateId, validatePageCreate, validatePageUpdate } from '../../../foundation/validation'
|
||||
import { validateId, validatePageCreate, validatePageUpdate } from '../../../foundation/validation'
|
||||
|
||||
export interface PageOperations {
|
||||
create: (data: PageCreatePayload) => Promise<PageView>
|
||||
read: (id: string) => Promise<PageView | null>
|
||||
readBySlug: (slug: string) => Promise<PageView | null>
|
||||
update: (id: string, data: Partial<PageView>) => Promise<PageView>
|
||||
export interface PageConfigOperations {
|
||||
create: (data: CreatePageInput) => Promise<PageConfig>
|
||||
read: (id: string) => Promise<PageConfig | null>
|
||||
readByPath: (path: string) => Promise<PageConfig | null>
|
||||
update: (id: string, data: UpdatePageInput) => Promise<PageConfig>
|
||||
delete: (id: string) => Promise<boolean>
|
||||
list: (options?: ListOptions) => Promise<ListResult<PageView>>
|
||||
list: (options?: ListOptions) => Promise<ListResult<PageConfig>>
|
||||
}
|
||||
|
||||
type PageCreatePayload = Omit<PageView, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'> & { tenantId?: string }
|
||||
|
||||
const assertValidId = (id: string) => {
|
||||
const errors = validateId(id)
|
||||
if (errors.length > 0) {
|
||||
@@ -26,27 +25,27 @@ const assertValidId = (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const assertValidSlug = (slug: string) => {
|
||||
if (!slug || !isValidSlug(slug)) {
|
||||
throw DBALError.validationError('Invalid page slug', [{ field: 'slug', error: 'slug is invalid' }])
|
||||
const assertValidPath = (path: string) => {
|
||||
if (!path || typeof path !== 'string' || path.trim().length === 0) {
|
||||
throw DBALError.validationError('Invalid page path', [{ field: 'path', error: 'path is invalid' }])
|
||||
}
|
||||
}
|
||||
|
||||
const assertValidCreate = (data: PageCreatePayload) => {
|
||||
const assertValidCreate = (data: CreatePageInput) => {
|
||||
const errors = validatePageCreate(data)
|
||||
if (errors.length > 0) {
|
||||
throw DBALError.validationError('Invalid page data', errors.map(error => ({ field: 'page', error })))
|
||||
}
|
||||
}
|
||||
|
||||
const assertValidUpdate = (data: Partial<PageView>) => {
|
||||
const assertValidUpdate = (data: UpdatePageInput) => {
|
||||
const errors = validatePageUpdate(data)
|
||||
if (errors.length > 0) {
|
||||
throw DBALError.validationError('Invalid page update data', errors.map(error => ({ field: 'page', error })))
|
||||
}
|
||||
}
|
||||
|
||||
const resolveTenantId = (configuredTenantId?: string, data?: Partial<PageView>): string | null => {
|
||||
const resolveTenantId = (configuredTenantId?: string, data?: Partial<PageConfig>): string | null => {
|
||||
if (configuredTenantId && configuredTenantId.length > 0) return configuredTenantId
|
||||
const candidate = data?.tenantId
|
||||
if (typeof candidate === 'string' && candidate.length > 0) return candidate
|
||||
@@ -67,21 +66,44 @@ const resolveTenantFilter = (
|
||||
return null
|
||||
}
|
||||
|
||||
export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): PageOperations => ({
|
||||
const withPageDefaults = (data: CreatePageInput): PageConfig => {
|
||||
const now = BigInt(Date.now())
|
||||
return {
|
||||
id: data.id ?? randomUUID(),
|
||||
tenantId: data.tenantId ?? null,
|
||||
packageId: data.packageId ?? null,
|
||||
path: data.path,
|
||||
title: data.title,
|
||||
description: data.description ?? null,
|
||||
icon: data.icon ?? null,
|
||||
component: data.component ?? null,
|
||||
componentTree: data.componentTree,
|
||||
level: data.level,
|
||||
requiresAuth: data.requiresAuth,
|
||||
requiredRole: data.requiredRole ?? null,
|
||||
parentPath: data.parentPath ?? null,
|
||||
sortOrder: data.sortOrder ?? 0,
|
||||
isPublished: data.isPublished ?? true,
|
||||
params: data.params ?? null,
|
||||
meta: data.meta ?? null,
|
||||
createdAt: data.createdAt ?? now,
|
||||
updatedAt: data.updatedAt ?? now,
|
||||
}
|
||||
}
|
||||
|
||||
export const createPageConfigOperations = (adapter: DBALAdapter, tenantId?: string): PageConfigOperations => ({
|
||||
create: async data => {
|
||||
const resolvedTenantId = resolveTenantId(tenantId, data)
|
||||
if (!resolvedTenantId) {
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
const isActive = data.isActive ?? true
|
||||
const payload = { ...data, tenantId: resolvedTenantId, isActive }
|
||||
const payload = withPageDefaults({ ...data, tenantId: resolvedTenantId })
|
||||
assertValidCreate(payload)
|
||||
try {
|
||||
return adapter.create('PageView', payload) as Promise<PageView>
|
||||
return adapter.create('PageConfig', payload) as Promise<PageConfig>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
const slug = typeof data.slug === 'string' ? data.slug : 'unknown'
|
||||
throw DBALError.conflict(`Page with slug '${slug}' already exists`)
|
||||
throw DBALError.conflict(`Page with path '${data.path}' already exists`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -92,21 +114,21 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidId(id)
|
||||
const result = await adapter.findFirst('PageView', { id, tenantId: resolvedTenantId }) as PageView | null
|
||||
const result = await adapter.findFirst('PageConfig', { id, tenantId: resolvedTenantId }) as PageConfig | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
readBySlug: async slug => {
|
||||
readByPath: async path => {
|
||||
const resolvedTenantId = resolveTenantId(tenantId)
|
||||
if (!resolvedTenantId) {
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidSlug(slug)
|
||||
const result = await adapter.findFirst('PageView', { slug, tenantId: resolvedTenantId }) as PageView | null
|
||||
assertValidPath(path)
|
||||
const result = await adapter.findFirst('PageConfig', { path, tenantId: resolvedTenantId }) as PageConfig | null
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found with slug: ${slug}`)
|
||||
throw DBALError.notFound(`Page not found with path: ${path}`)
|
||||
}
|
||||
return result
|
||||
},
|
||||
@@ -120,18 +142,15 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
|
||||
}
|
||||
assertValidId(id)
|
||||
assertValidUpdate(data)
|
||||
const existing = await adapter.findFirst('PageView', { id, tenantId: resolvedTenantId }) as PageView | null
|
||||
const existing = await adapter.findFirst('PageConfig', { id, tenantId: resolvedTenantId }) as PageConfig | null
|
||||
if (!existing) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
try {
|
||||
return adapter.update('PageView', id, data) as Promise<PageView>
|
||||
return adapter.update('PageConfig', id, data) as Promise<PageConfig>
|
||||
} catch (error) {
|
||||
if (error instanceof DBALError && error.code === 409) {
|
||||
if (typeof data.slug === 'string') {
|
||||
throw DBALError.conflict(`Page with slug '${data.slug}' already exists`)
|
||||
}
|
||||
throw DBALError.conflict('Page slug already exists')
|
||||
throw DBALError.conflict('Page path already exists')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -142,11 +161,11 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
assertValidId(id)
|
||||
const existing = await adapter.findFirst('PageView', { id, tenantId: resolvedTenantId }) as PageView | null
|
||||
const existing = await adapter.findFirst('PageConfig', { id, tenantId: resolvedTenantId }) as PageConfig | null
|
||||
if (!existing) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
const result = await adapter.delete('PageView', id)
|
||||
const result = await adapter.delete('PageConfig', id)
|
||||
if (!result) {
|
||||
throw DBALError.notFound(`Page not found: ${id}`)
|
||||
}
|
||||
@@ -157,6 +176,8 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
|
||||
if (!tenantFilter) {
|
||||
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
|
||||
}
|
||||
return adapter.list('PageView', { ...options, filter: tenantFilter }) as Promise<ListResult<PageView>>
|
||||
return adapter.list('PageConfig', { ...options, filter: tenantFilter }) as Promise<ListResult<PageConfig>>
|
||||
},
|
||||
})
|
||||
|
||||
export const createPageOperations = createPageConfigOperations
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
export interface Credential {
|
||||
id: string
|
||||
username: string
|
||||
passwordHash: string
|
||||
firstLogin: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
userId: string
|
||||
token: string
|
||||
expiresAt: Date
|
||||
createdAt: Date
|
||||
lastActivity: Date
|
||||
expiresAt: bigint
|
||||
createdAt: bigint
|
||||
lastActivity: bigint
|
||||
ipAddress?: string | null
|
||||
userAgent?: string | null
|
||||
}
|
||||
|
||||
export interface CreateSessionInput {
|
||||
id?: string
|
||||
userId: string
|
||||
token: string
|
||||
expiresAt: Date
|
||||
expiresAt: bigint
|
||||
createdAt?: bigint
|
||||
lastActivity?: bigint
|
||||
ipAddress?: string | null
|
||||
userAgent?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateSessionInput {
|
||||
userId?: string
|
||||
token?: string
|
||||
expiresAt?: Date
|
||||
lastActivity?: Date
|
||||
expiresAt?: bigint
|
||||
lastActivity?: bigint
|
||||
ipAddress?: string | null
|
||||
userAgent?: string | null
|
||||
}
|
||||
|
||||
@@ -1,71 +1,90 @@
|
||||
export interface Workflow {
|
||||
id: string
|
||||
tenantId: string
|
||||
name: string
|
||||
description?: string
|
||||
trigger: 'manual' | 'schedule' | 'event' | 'webhook'
|
||||
triggerConfig: Record<string, unknown>
|
||||
steps: Record<string, unknown>
|
||||
isActive: boolean
|
||||
createdBy: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
nodes: string
|
||||
edges: string
|
||||
enabled: boolean
|
||||
version: number
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
createdBy?: string | null
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
export interface CreateWorkflowInput {
|
||||
tenantId?: string
|
||||
id?: string
|
||||
name: string
|
||||
description?: string
|
||||
trigger: Workflow['trigger']
|
||||
triggerConfig: Record<string, unknown>
|
||||
steps: Record<string, unknown>
|
||||
isActive?: boolean
|
||||
createdBy: string
|
||||
nodes: string
|
||||
edges: string
|
||||
enabled: boolean
|
||||
version?: number
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
createdBy?: string | null
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateWorkflowInput {
|
||||
tenantId?: string
|
||||
name?: string
|
||||
description?: string
|
||||
trigger?: Workflow['trigger']
|
||||
triggerConfig?: Record<string, unknown>
|
||||
steps?: Record<string, unknown>
|
||||
isActive?: boolean
|
||||
createdBy?: string
|
||||
nodes?: string
|
||||
edges?: string
|
||||
enabled?: boolean
|
||||
version?: number
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
createdBy?: string | null
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
export interface LuaScript {
|
||||
id: string
|
||||
tenantId: string
|
||||
name: string
|
||||
description?: string
|
||||
code: string
|
||||
parameters: string
|
||||
returnType?: string | null
|
||||
isSandboxed: boolean
|
||||
allowedGlobals: string[]
|
||||
allowedGlobals: string
|
||||
timeoutMs: number
|
||||
createdBy: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
version: number
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
createdBy?: string | null
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
export interface CreateLuaScriptInput {
|
||||
tenantId?: string
|
||||
id?: string
|
||||
name: string
|
||||
description?: string
|
||||
code: string
|
||||
parameters: string
|
||||
returnType?: string | null
|
||||
isSandboxed?: boolean
|
||||
allowedGlobals: string[]
|
||||
allowedGlobals: string
|
||||
timeoutMs?: number
|
||||
createdBy: string
|
||||
version?: number
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
createdBy?: string | null
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateLuaScriptInput {
|
||||
tenantId?: string
|
||||
name?: string
|
||||
description?: string
|
||||
code?: string
|
||||
parameters?: string
|
||||
returnType?: string | null
|
||||
isSandboxed?: boolean
|
||||
allowedGlobals?: string[]
|
||||
allowedGlobals?: string
|
||||
timeoutMs?: number
|
||||
createdBy?: string
|
||||
version?: number
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
createdBy?: string | null
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
@@ -1,44 +1,116 @@
|
||||
export interface PageView {
|
||||
export interface PageConfig {
|
||||
id: string
|
||||
tenantId: string
|
||||
slug: string
|
||||
tenantId?: string | null
|
||||
packageId?: string | null
|
||||
path: string
|
||||
title: string
|
||||
description?: string
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
component?: string | null
|
||||
componentTree: string
|
||||
level: number
|
||||
layout: Record<string, unknown>
|
||||
isActive: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
requiresAuth: boolean
|
||||
requiredRole?: string | null
|
||||
parentPath?: string | null
|
||||
sortOrder: number
|
||||
isPublished: boolean
|
||||
params?: string | null
|
||||
meta?: string | null
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
}
|
||||
|
||||
export interface CreatePageInput {
|
||||
tenantId?: string
|
||||
slug: string
|
||||
id?: string
|
||||
tenantId?: string | null
|
||||
packageId?: string | null
|
||||
path: string
|
||||
title: string
|
||||
description?: string
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
component?: string | null
|
||||
componentTree: string
|
||||
level: number
|
||||
layout: Record<string, unknown>
|
||||
isActive?: boolean
|
||||
requiresAuth: boolean
|
||||
requiredRole?: string | null
|
||||
parentPath?: string | null
|
||||
sortOrder?: number
|
||||
isPublished?: boolean
|
||||
params?: string | null
|
||||
meta?: string | null
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
}
|
||||
|
||||
export interface UpdatePageInput {
|
||||
tenantId?: string
|
||||
slug?: string
|
||||
tenantId?: string | null
|
||||
packageId?: string | null
|
||||
path?: string
|
||||
title?: string
|
||||
description?: string
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
component?: string | null
|
||||
componentTree?: string
|
||||
level?: number
|
||||
layout?: Record<string, unknown>
|
||||
isActive?: boolean
|
||||
requiresAuth?: boolean
|
||||
requiredRole?: string | null
|
||||
parentPath?: string | null
|
||||
sortOrder?: number
|
||||
isPublished?: boolean
|
||||
params?: string | null
|
||||
meta?: string | null
|
||||
createdAt?: bigint | null
|
||||
updatedAt?: bigint | null
|
||||
}
|
||||
|
||||
export interface ComponentHierarchy {
|
||||
export interface ComponentNode {
|
||||
id: string
|
||||
tenantId: string
|
||||
pageId: string
|
||||
parentId?: string
|
||||
componentType: string
|
||||
type: string
|
||||
parentId?: string | null
|
||||
childIds: string
|
||||
order: number
|
||||
props: Record<string, unknown>
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
pageId: string
|
||||
}
|
||||
|
||||
export interface CreateComponentNodeInput {
|
||||
id?: string
|
||||
type: string
|
||||
parentId?: string | null
|
||||
childIds: string
|
||||
order: number
|
||||
pageId: string
|
||||
}
|
||||
|
||||
export interface UpdateComponentNodeInput {
|
||||
type?: string
|
||||
parentId?: string | null
|
||||
childIds?: string
|
||||
order?: number
|
||||
pageId?: string
|
||||
}
|
||||
|
||||
export interface ComponentConfig {
|
||||
id: string
|
||||
componentId: string
|
||||
props: string
|
||||
styles: string
|
||||
events: string
|
||||
conditionalRendering?: string | null
|
||||
}
|
||||
|
||||
export interface CreateComponentConfigInput {
|
||||
id?: string
|
||||
componentId: string
|
||||
props: string
|
||||
styles: string
|
||||
events: string
|
||||
conditionalRendering?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateComponentConfigInput {
|
||||
componentId?: string
|
||||
props?: string
|
||||
styles?: string
|
||||
events?: string
|
||||
conditionalRendering?: string | null
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface SoftDeletableEntity extends BaseEntity {
|
||||
}
|
||||
|
||||
export interface TenantScopedEntity extends BaseEntity {
|
||||
tenantId: string
|
||||
tenantId?: string | null
|
||||
}
|
||||
|
||||
export interface EntityMetadata {
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
export interface Package {
|
||||
id: string
|
||||
tenantId: string
|
||||
name: string
|
||||
export interface InstalledPackage {
|
||||
packageId: string
|
||||
tenantId?: string | null
|
||||
installedAt: bigint
|
||||
version: string
|
||||
description?: string
|
||||
author: string
|
||||
manifest: Record<string, unknown>
|
||||
isInstalled: boolean
|
||||
installedAt?: Date
|
||||
installedBy?: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
enabled: boolean
|
||||
config?: string | null
|
||||
}
|
||||
|
||||
export interface CreatePackageInput {
|
||||
tenantId?: string
|
||||
name: string
|
||||
packageId: string
|
||||
tenantId?: string | null
|
||||
installedAt: bigint
|
||||
version: string
|
||||
description?: string
|
||||
author: string
|
||||
manifest: Record<string, unknown>
|
||||
isInstalled?: boolean
|
||||
installedAt?: Date
|
||||
installedBy?: string
|
||||
enabled: boolean
|
||||
config?: string | null
|
||||
}
|
||||
|
||||
export interface UpdatePackageInput {
|
||||
tenantId?: string
|
||||
name?: string
|
||||
tenantId?: string | null
|
||||
installedAt?: bigint
|
||||
version?: string
|
||||
description?: string
|
||||
author?: string
|
||||
manifest?: Record<string, unknown>
|
||||
isInstalled?: boolean
|
||||
installedAt?: Date
|
||||
installedBy?: string
|
||||
enabled?: boolean
|
||||
config?: string | null
|
||||
}
|
||||
|
||||
export interface PackageData {
|
||||
packageId: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface CreatePackageDataInput {
|
||||
packageId: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface UpdatePackageDataInput {
|
||||
data?: string
|
||||
}
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
export type UserRole = 'public' | 'user' | 'moderator' | 'admin' | 'god' | 'supergod'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
tenantId: string
|
||||
username: string
|
||||
email: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
role: UserRole
|
||||
profilePicture?: string | null
|
||||
bio?: string | null
|
||||
createdAt: bigint
|
||||
tenantId?: string | null
|
||||
isInstanceOwner: boolean
|
||||
passwordChangeTimestamp?: bigint | null
|
||||
firstLogin: boolean
|
||||
}
|
||||
|
||||
export interface CreateUserInput {
|
||||
tenantId?: string
|
||||
id?: string
|
||||
username: string
|
||||
email: string
|
||||
role?: User['role']
|
||||
role: UserRole
|
||||
profilePicture?: string | null
|
||||
bio?: string | null
|
||||
createdAt?: bigint
|
||||
tenantId?: string | null
|
||||
isInstanceOwner?: boolean
|
||||
passwordChangeTimestamp?: bigint | null
|
||||
firstLogin?: boolean
|
||||
}
|
||||
|
||||
export interface UpdateUserInput {
|
||||
tenantId?: string
|
||||
username?: string
|
||||
email?: string
|
||||
role?: User['role']
|
||||
role?: UserRole
|
||||
profilePicture?: string | null
|
||||
bio?: string | null
|
||||
tenantId?: string | null
|
||||
isInstanceOwner?: boolean
|
||||
passwordChangeTimestamp?: bigint | null
|
||||
firstLogin?: boolean
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export { isValidTitle } from './validation/is-valid-title'
|
||||
export { isValidLevel } from './validation/is-valid-level'
|
||||
export { isValidUuid } from './validation/is-valid-uuid'
|
||||
export { isPlainObject } from './validation/is-plain-object'
|
||||
export { isValidJsonString } from './validation/is-valid-json'
|
||||
export { validateUserCreate } from './validation/validate-user-create'
|
||||
export { validateUserUpdate } from './validation/validate-user-update'
|
||||
export { validateCredentialCreate } from './validation/validate-credential-create'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { isValidJsonString as isValidJsonStringPredicate } from '../../validation/predicates/string/is-valid-json'
|
||||
|
||||
export const isValidJsonString = (value: string): boolean => isValidJsonStringPredicate(value)
|
||||
@@ -1,38 +1,43 @@
|
||||
/**
|
||||
* @file types.ts
|
||||
* @description Core DBAL types (stub)
|
||||
* @description Core DBAL types
|
||||
*/
|
||||
|
||||
export interface DBALError {
|
||||
code: string;
|
||||
message: string;
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface Result<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: DBALError;
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: DBALError
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
filter?: Record<string, unknown>;
|
||||
sort?: Record<string, 'asc' | 'desc'>;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
filter?: Record<string, unknown>
|
||||
sort?: Record<string, 'asc' | 'desc'>
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface ListResult<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
hasMore: boolean;
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
export type User = any;
|
||||
export type Session = any;
|
||||
export type Page = any;
|
||||
export type PageView = any;
|
||||
export type Workflow = any;
|
||||
export type LuaScript = any;
|
||||
export type Component = any;
|
||||
export type {
|
||||
User,
|
||||
Credential,
|
||||
Session,
|
||||
Workflow,
|
||||
LuaScript,
|
||||
PageConfig,
|
||||
ComponentNode,
|
||||
ComponentConfig,
|
||||
InstalledPackage,
|
||||
PackageData,
|
||||
} from './foundation/types'
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import type { ComponentHierarchy } from '../types'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
import type { ComponentNode } from '../types'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validateComponentHierarchyCreate(data: Partial<ComponentHierarchy>): string[] {
|
||||
export function validateComponentHierarchyCreate(data: Partial<ComponentNode>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.pageId) {
|
||||
errors.push('pageId is required')
|
||||
} else if (!isValidUuid(data.pageId)) {
|
||||
errors.push('pageId must be a valid UUID')
|
||||
} else if (typeof data.pageId !== 'string' || data.pageId.trim().length === 0) {
|
||||
errors.push('pageId must be a non-empty string')
|
||||
}
|
||||
|
||||
if (data.parentId !== undefined) {
|
||||
if (typeof data.parentId !== 'string' || !isValidUuid(data.parentId)) {
|
||||
errors.push('parentId must be a valid UUID')
|
||||
if (data.parentId !== null && (typeof data.parentId !== 'string' || data.parentId.trim().length === 0)) {
|
||||
errors.push('parentId must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.componentType) {
|
||||
errors.push('componentType is required')
|
||||
} else if (data.componentType.length > 100) {
|
||||
errors.push('componentType must be 1-100 characters')
|
||||
if (!data.type) {
|
||||
errors.push('type is required')
|
||||
} else if (typeof data.type !== 'string' || data.type.length > 100) {
|
||||
errors.push('type must be 1-100 characters')
|
||||
}
|
||||
|
||||
if (!data.childIds) {
|
||||
errors.push('childIds is required')
|
||||
} else if (typeof data.childIds !== 'string' || !isValidJsonString(data.childIds)) {
|
||||
errors.push('childIds must be a JSON string')
|
||||
}
|
||||
|
||||
if (data.order === undefined) {
|
||||
@@ -29,11 +34,5 @@ export function validateComponentHierarchyCreate(data: Partial<ComponentHierarch
|
||||
errors.push('order must be a non-negative integer')
|
||||
}
|
||||
|
||||
if (data.props === undefined) {
|
||||
errors.push('props is required')
|
||||
} else if (!isPlainObject(data.props)) {
|
||||
errors.push('props must be an object')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import type { ComponentHierarchy } from '../types'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
import type { ComponentNode } from '../types'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validateComponentHierarchyUpdate(data: Partial<ComponentHierarchy>): string[] {
|
||||
export function validateComponentHierarchyUpdate(data: Partial<ComponentNode>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.pageId !== undefined) {
|
||||
if (typeof data.pageId !== 'string' || !isValidUuid(data.pageId)) {
|
||||
errors.push('pageId must be a valid UUID')
|
||||
if (typeof data.pageId !== 'string' || data.pageId.trim().length === 0) {
|
||||
errors.push('pageId must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.parentId !== undefined) {
|
||||
if (typeof data.parentId !== 'string' || !isValidUuid(data.parentId)) {
|
||||
errors.push('parentId must be a valid UUID')
|
||||
if (data.parentId !== null && (typeof data.parentId !== 'string' || data.parentId.trim().length === 0)) {
|
||||
errors.push('parentId must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.componentType !== undefined) {
|
||||
if (typeof data.componentType !== 'string' || data.componentType.length === 0 || data.componentType.length > 100) {
|
||||
errors.push('componentType must be 1-100 characters')
|
||||
if (data.type !== undefined) {
|
||||
if (typeof data.type !== 'string' || data.type.length === 0 || data.type.length > 100) {
|
||||
errors.push('type must be 1-100 characters')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.childIds !== undefined) {
|
||||
if (typeof data.childIds !== 'string' || !isValidJsonString(data.childIds)) {
|
||||
errors.push('childIds must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +34,5 @@ export function validateComponentHierarchyUpdate(data: Partial<ComponentHierarch
|
||||
}
|
||||
}
|
||||
|
||||
if (data.props !== undefined && !isPlainObject(data.props)) {
|
||||
errors.push('props must be an object')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -16,11 +16,5 @@ export function validateCredentialCreate(data: Partial<Credential>): string[] {
|
||||
errors.push('passwordHash must be a non-empty string')
|
||||
}
|
||||
|
||||
if (data.firstLogin === undefined) {
|
||||
errors.push('firstLogin is required')
|
||||
} else if (typeof data.firstLogin !== 'boolean') {
|
||||
errors.push('firstLogin must be a boolean')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -16,9 +16,5 @@ export function validateCredentialUpdate(data: Partial<Credential>): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') {
|
||||
errors.push('firstLogin must be a boolean')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LuaScript } from '../types'
|
||||
import { isAllowedLuaGlobal } from '../../predicates/lua/is-allowed-lua-global'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
|
||||
const errors: string[] = []
|
||||
@@ -17,6 +17,16 @@ export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
|
||||
errors.push('code must be a non-empty string')
|
||||
}
|
||||
|
||||
if (!data.parameters) {
|
||||
errors.push('parameters is required')
|
||||
} else if (typeof data.parameters !== 'string' || !isValidJsonString(data.parameters)) {
|
||||
errors.push('parameters must be a JSON string')
|
||||
}
|
||||
|
||||
if (data.returnType !== undefined && data.returnType !== null && typeof data.returnType !== 'string') {
|
||||
errors.push('returnType must be a string')
|
||||
}
|
||||
|
||||
if (data.isSandboxed === undefined) {
|
||||
errors.push('isSandboxed is required')
|
||||
} else if (typeof data.isSandboxed !== 'boolean') {
|
||||
@@ -25,14 +35,23 @@ export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
|
||||
|
||||
if (data.allowedGlobals === undefined) {
|
||||
errors.push('allowedGlobals is required')
|
||||
} else if (!Array.isArray(data.allowedGlobals)) {
|
||||
errors.push('allowedGlobals must be an array of strings')
|
||||
} else if (data.allowedGlobals.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
|
||||
errors.push('allowedGlobals must contain non-empty strings')
|
||||
} else if (typeof data.allowedGlobals !== 'string' || !isValidJsonString(data.allowedGlobals)) {
|
||||
errors.push('allowedGlobals must be a JSON string')
|
||||
} else {
|
||||
const invalidGlobals = data.allowedGlobals.filter((entry) => !isAllowedLuaGlobal(entry))
|
||||
if (invalidGlobals.length > 0) {
|
||||
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
|
||||
try {
|
||||
const parsed = JSON.parse(data.allowedGlobals) as unknown
|
||||
if (!Array.isArray(parsed)) {
|
||||
errors.push('allowedGlobals must be a JSON array')
|
||||
} else if (parsed.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
|
||||
errors.push('allowedGlobals must contain non-empty strings')
|
||||
} else {
|
||||
const invalidGlobals = parsed.filter((entry) => !isAllowedLuaGlobal(entry))
|
||||
if (invalidGlobals.length > 0) {
|
||||
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
errors.push('allowedGlobals must be valid JSON')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +61,26 @@ export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
|
||||
errors.push('timeoutMs must be an integer between 100 and 30000')
|
||||
}
|
||||
|
||||
if (!data.createdBy) {
|
||||
errors.push('createdBy is required')
|
||||
} else if (!isValidUuid(data.createdBy)) {
|
||||
errors.push('createdBy must be a valid UUID')
|
||||
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
|
||||
errors.push('version must be a positive integer')
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
|
||||
errors.push('updatedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.createdBy !== undefined && data.createdBy !== null) {
|
||||
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
|
||||
errors.push('createdBy must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LuaScript } from '../types'
|
||||
import { isAllowedLuaGlobal } from '../../predicates/lua/is-allowed-lua-global'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validateLuaScriptUpdate(data: Partial<LuaScript>): string[] {
|
||||
const errors: string[] = []
|
||||
@@ -17,19 +17,38 @@ export function validateLuaScriptUpdate(data: Partial<LuaScript>): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.parameters !== undefined) {
|
||||
if (typeof data.parameters !== 'string' || !isValidJsonString(data.parameters)) {
|
||||
errors.push('parameters must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.returnType !== undefined && data.returnType !== null && typeof data.returnType !== 'string') {
|
||||
errors.push('returnType must be a string')
|
||||
}
|
||||
|
||||
if (data.isSandboxed !== undefined && typeof data.isSandboxed !== 'boolean') {
|
||||
errors.push('isSandboxed must be a boolean')
|
||||
}
|
||||
|
||||
if (data.allowedGlobals !== undefined) {
|
||||
if (!Array.isArray(data.allowedGlobals)) {
|
||||
errors.push('allowedGlobals must be an array of strings')
|
||||
} else if (data.allowedGlobals.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
|
||||
errors.push('allowedGlobals must contain non-empty strings')
|
||||
if (typeof data.allowedGlobals !== 'string' || !isValidJsonString(data.allowedGlobals)) {
|
||||
errors.push('allowedGlobals must be a JSON string')
|
||||
} else {
|
||||
const invalidGlobals = data.allowedGlobals.filter((entry) => !isAllowedLuaGlobal(entry))
|
||||
if (invalidGlobals.length > 0) {
|
||||
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
|
||||
try {
|
||||
const parsed = JSON.parse(data.allowedGlobals) as unknown
|
||||
if (!Array.isArray(parsed)) {
|
||||
errors.push('allowedGlobals must be a JSON array')
|
||||
} else if (parsed.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
|
||||
errors.push('allowedGlobals must contain non-empty strings')
|
||||
} else {
|
||||
const invalidGlobals = parsed.filter((entry) => !isAllowedLuaGlobal(entry))
|
||||
if (invalidGlobals.length > 0) {
|
||||
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
errors.push('allowedGlobals must be valid JSON')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,12 +59,28 @@ export function validateLuaScriptUpdate(data: Partial<LuaScript>): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.createdBy !== undefined) {
|
||||
if (typeof data.createdBy !== 'string' || !isValidUuid(data.createdBy)) {
|
||||
errors.push('createdBy must be a valid UUID')
|
||||
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
|
||||
errors.push('version must be a positive integer')
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
|
||||
errors.push('updatedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.createdBy !== undefined && data.createdBy !== null) {
|
||||
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
|
||||
errors.push('createdBy must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
errors.push('description must be a string')
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { Package } from '../types'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidDate } from '../../predicates/is-valid-date'
|
||||
import type { InstalledPackage } from '../types'
|
||||
import { isValidSemver } from '../../predicates/string/is-valid-semver'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validatePackageCreate(data: Partial<Package>): string[] {
|
||||
export function validatePackageCreate(data: Partial<InstalledPackage>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.name) {
|
||||
errors.push('name is required')
|
||||
} else if (typeof data.name !== 'string' || data.name.length > 255) {
|
||||
errors.push('name must be 1-255 characters')
|
||||
if (!data.packageId) {
|
||||
errors.push('packageId is required')
|
||||
} else if (typeof data.packageId !== 'string' || data.packageId.trim().length === 0) {
|
||||
errors.push('packageId must be a non-empty string')
|
||||
}
|
||||
|
||||
if (data.installedAt === undefined) {
|
||||
errors.push('installedAt is required')
|
||||
} else if (typeof data.installedAt !== 'bigint') {
|
||||
errors.push('installedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (!data.version) {
|
||||
@@ -19,36 +23,20 @@ export function validatePackageCreate(data: Partial<Package>): string[] {
|
||||
errors.push('version must be semantic (x.y.z)')
|
||||
}
|
||||
|
||||
if (!data.author) {
|
||||
errors.push('author is required')
|
||||
} else if (typeof data.author !== 'string' || data.author.length > 255) {
|
||||
errors.push('author must be 1-255 characters')
|
||||
if (data.enabled === undefined) {
|
||||
errors.push('enabled is required')
|
||||
} else if (typeof data.enabled !== 'boolean') {
|
||||
errors.push('enabled must be a boolean')
|
||||
}
|
||||
|
||||
if (data.manifest === undefined) {
|
||||
errors.push('manifest is required')
|
||||
} else if (!isPlainObject(data.manifest)) {
|
||||
errors.push('manifest must be an object')
|
||||
}
|
||||
|
||||
if (data.isInstalled === undefined) {
|
||||
errors.push('isInstalled is required')
|
||||
} else if (typeof data.isInstalled !== 'boolean') {
|
||||
errors.push('isInstalled must be a boolean')
|
||||
}
|
||||
|
||||
if (data.installedAt !== undefined && !isValidDate(data.installedAt)) {
|
||||
errors.push('installedAt must be a valid date')
|
||||
}
|
||||
|
||||
if (data.installedBy !== undefined) {
|
||||
if (typeof data.installedBy !== 'string' || !isValidUuid(data.installedBy)) {
|
||||
errors.push('installedBy must be a valid UUID')
|
||||
if (data.config !== undefined && data.config !== null) {
|
||||
if (typeof data.config !== 'string' || !isValidJsonString(data.config)) {
|
||||
errors.push('config must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
errors.push('description must be a string')
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { Package } from '../types'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidDate } from '../../predicates/is-valid-date'
|
||||
import type { InstalledPackage } from '../types'
|
||||
import { isValidSemver } from '../../predicates/string/is-valid-semver'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validatePackageUpdate(data: Partial<Package>): string[] {
|
||||
export function validatePackageUpdate(data: Partial<InstalledPackage>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.name !== undefined) {
|
||||
if (typeof data.name !== 'string' || data.name.length === 0 || data.name.length > 255) {
|
||||
errors.push('name must be 1-255 characters')
|
||||
}
|
||||
if (data.installedAt !== undefined && typeof data.installedAt !== 'bigint') {
|
||||
errors.push('installedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.version !== undefined) {
|
||||
@@ -19,32 +15,18 @@ export function validatePackageUpdate(data: Partial<Package>): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.author !== undefined) {
|
||||
if (typeof data.author !== 'string' || data.author.length === 0 || data.author.length > 255) {
|
||||
errors.push('author must be 1-255 characters')
|
||||
if (data.enabled !== undefined && typeof data.enabled !== 'boolean') {
|
||||
errors.push('enabled must be a boolean')
|
||||
}
|
||||
|
||||
if (data.config !== undefined && data.config !== null) {
|
||||
if (typeof data.config !== 'string' || !isValidJsonString(data.config)) {
|
||||
errors.push('config must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.manifest !== undefined && !isPlainObject(data.manifest)) {
|
||||
errors.push('manifest must be an object')
|
||||
}
|
||||
|
||||
if (data.isInstalled !== undefined && typeof data.isInstalled !== 'boolean') {
|
||||
errors.push('isInstalled must be a boolean')
|
||||
}
|
||||
|
||||
if (data.installedAt !== undefined && !isValidDate(data.installedAt)) {
|
||||
errors.push('installedAt must be a valid date')
|
||||
}
|
||||
|
||||
if (data.installedBy !== undefined) {
|
||||
if (typeof data.installedBy !== 'string' || !isValidUuid(data.installedBy)) {
|
||||
errors.push('installedBy must be a valid UUID')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
errors.push('description must be a string')
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import type { PageView } from '../types'
|
||||
import type { PageConfig } from '../types'
|
||||
import { isValidLevel } from '../../predicates/is-valid-level'
|
||||
import { isValidSlug } from '../../predicates/string/is-valid-slug'
|
||||
import { isValidTitle } from '../../predicates/string/is-valid-title'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
/**
|
||||
* Validation rules for PageView entity
|
||||
* Validation rules for PageConfig entity
|
||||
*/
|
||||
export function validatePageCreate(data: Partial<PageView>): string[] {
|
||||
export function validatePageCreate(data: Partial<PageConfig>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.slug) {
|
||||
errors.push('Slug is required')
|
||||
} else if (!isValidSlug(data.slug)) {
|
||||
errors.push('Invalid slug format (lowercase alphanumeric, hyphen, slash, 1-255 chars)')
|
||||
if (!data.path) {
|
||||
errors.push('path is required')
|
||||
} else if (typeof data.path !== 'string' || data.path.trim().length === 0 || data.path.length > 255) {
|
||||
errors.push('path must be 1-255 characters')
|
||||
}
|
||||
|
||||
if (!data.title) {
|
||||
@@ -22,27 +21,79 @@ export function validatePageCreate(data: Partial<PageView>): string[] {
|
||||
errors.push('Invalid title (must be 1-255 characters)')
|
||||
}
|
||||
|
||||
if (!data.componentTree) {
|
||||
errors.push('componentTree is required')
|
||||
} else if (typeof data.componentTree !== 'string' || !isValidJsonString(data.componentTree)) {
|
||||
errors.push('componentTree must be a JSON string')
|
||||
}
|
||||
|
||||
if (data.level === undefined) {
|
||||
errors.push('Level is required')
|
||||
errors.push('level is required')
|
||||
} else if (!isValidLevel(data.level)) {
|
||||
errors.push('Invalid level (must be 1-5)')
|
||||
errors.push('level must be between 1 and 6')
|
||||
}
|
||||
|
||||
if (data.layout === undefined) {
|
||||
errors.push('Layout is required')
|
||||
} else if (!isPlainObject(data.layout)) {
|
||||
errors.push('Layout must be an object')
|
||||
if (data.requiresAuth === undefined) {
|
||||
errors.push('requiresAuth is required')
|
||||
} else if (typeof data.requiresAuth !== 'boolean') {
|
||||
errors.push('requiresAuth must be a boolean')
|
||||
}
|
||||
|
||||
if (data.isActive === undefined) {
|
||||
errors.push('isActive is required')
|
||||
} else if (typeof data.isActive !== 'boolean') {
|
||||
errors.push('isActive must be a boolean')
|
||||
if (data.isPublished !== undefined && typeof data.isPublished !== 'boolean') {
|
||||
errors.push('isPublished must be a boolean')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
if (data.sortOrder !== undefined && (!Number.isInteger(data.sortOrder) || data.sortOrder < 0)) {
|
||||
errors.push('sortOrder must be a non-negative integer')
|
||||
}
|
||||
|
||||
if (data.params !== undefined && data.params !== null) {
|
||||
if (typeof data.params !== 'string' || !isValidJsonString(data.params)) {
|
||||
errors.push('params must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.meta !== undefined && data.meta !== null) {
|
||||
if (typeof data.meta !== 'string' || !isValidJsonString(data.meta)) {
|
||||
errors.push('meta must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
|
||||
errors.push('updatedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && data.description !== null && typeof data.description !== 'string') {
|
||||
errors.push('Description must be a string')
|
||||
}
|
||||
|
||||
if (data.icon !== undefined && data.icon !== null && typeof data.icon !== 'string') {
|
||||
errors.push('icon must be a string')
|
||||
}
|
||||
|
||||
if (data.component !== undefined && data.component !== null && typeof data.component !== 'string') {
|
||||
errors.push('component must be a string')
|
||||
}
|
||||
|
||||
if (data.requiredRole !== undefined && data.requiredRole !== null && typeof data.requiredRole !== 'string') {
|
||||
errors.push('requiredRole must be a string')
|
||||
}
|
||||
|
||||
if (data.parentPath !== undefined && data.parentPath !== null && typeof data.parentPath !== 'string') {
|
||||
errors.push('parentPath must be a string')
|
||||
}
|
||||
|
||||
if (data.packageId !== undefined && data.packageId !== null && typeof data.packageId !== 'string') {
|
||||
errors.push('packageId must be a string')
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1,35 +1,90 @@
|
||||
import type { PageView } from '../types'
|
||||
import type { PageConfig } from '../types'
|
||||
import { isValidLevel } from '../../predicates/is-valid-level'
|
||||
import { isValidSlug } from '../../predicates/string/is-valid-slug'
|
||||
import { isValidTitle } from '../../predicates/string/is-valid-title'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validatePageUpdate(data: Partial<PageView>): string[] {
|
||||
export function validatePageUpdate(data: Partial<PageConfig>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.slug !== undefined && !isValidSlug(data.slug)) {
|
||||
errors.push('Invalid slug format (lowercase alphanumeric, hyphen, slash, 1-255 chars)')
|
||||
if (data.path !== undefined) {
|
||||
if (typeof data.path !== 'string' || data.path.trim().length === 0 || data.path.length > 255) {
|
||||
errors.push('path must be 1-255 characters')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.title !== undefined && !isValidTitle(data.title)) {
|
||||
errors.push('Invalid title (must be 1-255 characters)')
|
||||
}
|
||||
|
||||
if (data.componentTree !== undefined) {
|
||||
if (typeof data.componentTree !== 'string' || !isValidJsonString(data.componentTree)) {
|
||||
errors.push('componentTree must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.level !== undefined && !isValidLevel(data.level)) {
|
||||
errors.push('Invalid level (must be 1-5)')
|
||||
errors.push('level must be between 1 and 6')
|
||||
}
|
||||
|
||||
if (data.layout !== undefined && !isPlainObject(data.layout)) {
|
||||
errors.push('Layout must be an object')
|
||||
if (data.requiresAuth !== undefined && typeof data.requiresAuth !== 'boolean') {
|
||||
errors.push('requiresAuth must be a boolean')
|
||||
}
|
||||
|
||||
if (data.isActive !== undefined && typeof data.isActive !== 'boolean') {
|
||||
errors.push('isActive must be a boolean')
|
||||
if (data.isPublished !== undefined && typeof data.isPublished !== 'boolean') {
|
||||
errors.push('isPublished must be a boolean')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
if (data.sortOrder !== undefined && (!Number.isInteger(data.sortOrder) || data.sortOrder < 0)) {
|
||||
errors.push('sortOrder must be a non-negative integer')
|
||||
}
|
||||
|
||||
if (data.params !== undefined && data.params !== null) {
|
||||
if (typeof data.params !== 'string' || !isValidJsonString(data.params)) {
|
||||
errors.push('params must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.meta !== undefined && data.meta !== null) {
|
||||
if (typeof data.meta !== 'string' || !isValidJsonString(data.meta)) {
|
||||
errors.push('meta must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
|
||||
errors.push('updatedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && data.description !== null && typeof data.description !== 'string') {
|
||||
errors.push('Description must be a string')
|
||||
}
|
||||
|
||||
if (data.icon !== undefined && data.icon !== null && typeof data.icon !== 'string') {
|
||||
errors.push('icon must be a string')
|
||||
}
|
||||
|
||||
if (data.component !== undefined && data.component !== null && typeof data.component !== 'string') {
|
||||
errors.push('component must be a string')
|
||||
}
|
||||
|
||||
if (data.requiredRole !== undefined && data.requiredRole !== null && typeof data.requiredRole !== 'string') {
|
||||
errors.push('requiredRole must be a string')
|
||||
}
|
||||
|
||||
if (data.parentPath !== undefined && data.parentPath !== null && typeof data.parentPath !== 'string') {
|
||||
errors.push('parentPath must be a string')
|
||||
}
|
||||
|
||||
if (data.packageId !== undefined && data.packageId !== null && typeof data.packageId !== 'string') {
|
||||
errors.push('packageId must be a string')
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { Session } from '../types'
|
||||
import { isValidDate } from '../../predicates/is-valid-date'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
|
||||
export function validateSessionCreate(data: Partial<Session>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!data.userId) {
|
||||
errors.push('userId is required')
|
||||
} else if (!isValidUuid(data.userId)) {
|
||||
errors.push('userId must be a valid UUID')
|
||||
} else if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
|
||||
errors.push('userId must be a non-empty string')
|
||||
}
|
||||
|
||||
if (!data.token) {
|
||||
@@ -19,8 +16,24 @@ export function validateSessionCreate(data: Partial<Session>): string[] {
|
||||
|
||||
if (data.expiresAt === undefined) {
|
||||
errors.push('expiresAt is required')
|
||||
} else if (!isValidDate(data.expiresAt)) {
|
||||
errors.push('expiresAt must be a valid date')
|
||||
} else if (typeof data.expiresAt !== 'bigint') {
|
||||
errors.push('expiresAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.lastActivity !== undefined && typeof data.lastActivity !== 'bigint') {
|
||||
errors.push('lastActivity must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.ipAddress !== undefined && data.ipAddress !== null && typeof data.ipAddress !== 'string') {
|
||||
errors.push('ipAddress must be a string')
|
||||
}
|
||||
|
||||
if (data.userAgent !== undefined && data.userAgent !== null && typeof data.userAgent !== 'string') {
|
||||
errors.push('userAgent must be a string')
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { Session } from '../types'
|
||||
import { isValidDate } from '../../predicates/is-valid-date'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
|
||||
export function validateSessionUpdate(data: Partial<Session>): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
if (data.userId !== undefined) {
|
||||
if (typeof data.userId !== 'string' || !isValidUuid(data.userId)) {
|
||||
errors.push('userId must be a valid UUID')
|
||||
if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
|
||||
errors.push('userId must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +14,20 @@ export function validateSessionUpdate(data: Partial<Session>): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.expiresAt !== undefined && !isValidDate(data.expiresAt)) {
|
||||
errors.push('expiresAt must be a valid date')
|
||||
if (data.expiresAt !== undefined && typeof data.expiresAt !== 'bigint') {
|
||||
errors.push('expiresAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.lastActivity !== undefined && !isValidDate(data.lastActivity)) {
|
||||
errors.push('lastActivity must be a valid date')
|
||||
if (data.lastActivity !== undefined && typeof data.lastActivity !== 'bigint') {
|
||||
errors.push('lastActivity must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.ipAddress !== undefined && data.ipAddress !== null && typeof data.ipAddress !== 'string') {
|
||||
errors.push('ipAddress must be a string')
|
||||
}
|
||||
|
||||
if (data.userAgent !== undefined && data.userAgent !== null && typeof data.userAgent !== 'string') {
|
||||
errors.push('userAgent must be a string')
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
/**
|
||||
* @file types.ts
|
||||
* @description Entity validation types (stub)
|
||||
* @description Entity validation types
|
||||
*/
|
||||
|
||||
import type {
|
||||
User,
|
||||
Credential,
|
||||
Session,
|
||||
PageConfig,
|
||||
ComponentNode,
|
||||
Workflow,
|
||||
LuaScript,
|
||||
InstalledPackage,
|
||||
PackageData,
|
||||
} from '../../foundation/types'
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export type User = any;
|
||||
export type Credential = any;
|
||||
export type Session = any;
|
||||
export type Page = any;
|
||||
export type PageView = any;
|
||||
export type ComponentHierarchy = any;
|
||||
export type Workflow = any;
|
||||
export type LuaScript = any;
|
||||
export type Package = any;
|
||||
export type { User, Credential, Session, PageConfig, ComponentNode, Workflow, LuaScript, InstalledPackage, PackageData }
|
||||
|
||||
@@ -22,9 +22,37 @@ export function validateUserCreate(data: Partial<User>): string[] {
|
||||
|
||||
if (!data.role) {
|
||||
errors.push('Role is required')
|
||||
} else if (!['user', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
} else if (!['public', 'user', 'moderator', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
errors.push('Invalid role')
|
||||
}
|
||||
|
||||
if (data.profilePicture !== undefined && data.profilePicture !== null && typeof data.profilePicture !== 'string') {
|
||||
errors.push('profilePicture must be a string')
|
||||
}
|
||||
|
||||
if (data.bio !== undefined && data.bio !== null && typeof data.bio !== 'string') {
|
||||
errors.push('bio must be a string')
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
if (data.isInstanceOwner !== undefined && typeof data.isInstanceOwner !== 'boolean') {
|
||||
errors.push('isInstanceOwner must be a boolean')
|
||||
}
|
||||
|
||||
if (data.passwordChangeTimestamp !== undefined && data.passwordChangeTimestamp !== null && typeof data.passwordChangeTimestamp !== 'bigint') {
|
||||
errors.push('passwordChangeTimestamp must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') {
|
||||
errors.push('firstLogin must be a boolean')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -13,9 +13,37 @@ export function validateUserUpdate(data: Partial<User>): string[] {
|
||||
errors.push('Invalid email format (max 255 chars)')
|
||||
}
|
||||
|
||||
if (data.role !== undefined && !['user', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
if (data.role !== undefined && !['public', 'user', 'moderator', 'admin', 'god', 'supergod'].includes(data.role)) {
|
||||
errors.push('Invalid role')
|
||||
}
|
||||
|
||||
if (data.profilePicture !== undefined && data.profilePicture !== null && typeof data.profilePicture !== 'string') {
|
||||
errors.push('profilePicture must be a string')
|
||||
}
|
||||
|
||||
if (data.bio !== undefined && data.bio !== null && typeof data.bio !== 'string') {
|
||||
errors.push('bio must be a string')
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
if (data.isInstanceOwner !== undefined && typeof data.isInstanceOwner !== 'boolean') {
|
||||
errors.push('isInstanceOwner must be a boolean')
|
||||
}
|
||||
|
||||
if (data.passwordChangeTimestamp !== undefined && data.passwordChangeTimestamp !== null && typeof data.passwordChangeTimestamp !== 'bigint') {
|
||||
errors.push('passwordChangeTimestamp must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') {
|
||||
errors.push('firstLogin must be a boolean')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { isValidUuid } from '../predicates/is-valid-uuid'
|
||||
|
||||
export function validateId(id: string): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
@@ -8,9 +6,5 @@ export function validateId(id: string): string[] {
|
||||
return errors
|
||||
}
|
||||
|
||||
if (!isValidUuid(id.trim())) {
|
||||
errors.push('ID must be a valid UUID')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Workflow } from '../types'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
|
||||
const triggerValues = ['manual', 'schedule', 'event', 'webhook'] as const
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validateWorkflowCreate(data: Partial<Workflow>): string[] {
|
||||
const errors: string[] = []
|
||||
@@ -13,34 +10,44 @@ export function validateWorkflowCreate(data: Partial<Workflow>): string[] {
|
||||
errors.push('name must be 1-255 characters')
|
||||
}
|
||||
|
||||
if (!data.trigger) {
|
||||
errors.push('trigger is required')
|
||||
} else if (!triggerValues.includes(data.trigger)) {
|
||||
errors.push('trigger must be one of manual, schedule, event, webhook')
|
||||
if (!data.nodes) {
|
||||
errors.push('nodes is required')
|
||||
} else if (typeof data.nodes !== 'string' || !isValidJsonString(data.nodes)) {
|
||||
errors.push('nodes must be a JSON string')
|
||||
}
|
||||
|
||||
if (data.triggerConfig === undefined) {
|
||||
errors.push('triggerConfig is required')
|
||||
} else if (!isPlainObject(data.triggerConfig)) {
|
||||
errors.push('triggerConfig must be an object')
|
||||
if (!data.edges) {
|
||||
errors.push('edges is required')
|
||||
} else if (typeof data.edges !== 'string' || !isValidJsonString(data.edges)) {
|
||||
errors.push('edges must be a JSON string')
|
||||
}
|
||||
|
||||
if (data.steps === undefined) {
|
||||
errors.push('steps is required')
|
||||
} else if (!isPlainObject(data.steps)) {
|
||||
errors.push('steps must be an object')
|
||||
if (data.enabled === undefined) {
|
||||
errors.push('enabled is required')
|
||||
} else if (typeof data.enabled !== 'boolean') {
|
||||
errors.push('enabled must be a boolean')
|
||||
}
|
||||
|
||||
if (data.isActive === undefined) {
|
||||
errors.push('isActive is required')
|
||||
} else if (typeof data.isActive !== 'boolean') {
|
||||
errors.push('isActive must be a boolean')
|
||||
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
|
||||
errors.push('version must be a positive integer')
|
||||
}
|
||||
|
||||
if (!data.createdBy) {
|
||||
errors.push('createdBy is required')
|
||||
} else if (!isValidUuid(data.createdBy)) {
|
||||
errors.push('createdBy must be a valid UUID')
|
||||
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
|
||||
errors.push('updatedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.createdBy !== undefined && data.createdBy !== null) {
|
||||
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
|
||||
errors.push('createdBy must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Workflow } from '../types'
|
||||
import { isPlainObject } from '../../predicates/is-plain-object'
|
||||
import { isValidUuid } from '../../predicates/is-valid-uuid'
|
||||
|
||||
const triggerValues = ['manual', 'schedule', 'event', 'webhook'] as const
|
||||
import { isValidJsonString } from '../../predicates/string/is-valid-json'
|
||||
|
||||
export function validateWorkflowUpdate(data: Partial<Workflow>): string[] {
|
||||
const errors: string[] = []
|
||||
@@ -13,28 +10,44 @@ export function validateWorkflowUpdate(data: Partial<Workflow>): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.trigger !== undefined && !triggerValues.includes(data.trigger)) {
|
||||
errors.push('trigger must be one of manual, schedule, event, webhook')
|
||||
}
|
||||
|
||||
if (data.triggerConfig !== undefined && !isPlainObject(data.triggerConfig)) {
|
||||
errors.push('triggerConfig must be an object')
|
||||
}
|
||||
|
||||
if (data.steps !== undefined && !isPlainObject(data.steps)) {
|
||||
errors.push('steps must be an object')
|
||||
}
|
||||
|
||||
if (data.isActive !== undefined && typeof data.isActive !== 'boolean') {
|
||||
errors.push('isActive must be a boolean')
|
||||
}
|
||||
|
||||
if (data.createdBy !== undefined) {
|
||||
if (typeof data.createdBy !== 'string' || !isValidUuid(data.createdBy)) {
|
||||
errors.push('createdBy must be a valid UUID')
|
||||
if (data.nodes !== undefined) {
|
||||
if (typeof data.nodes !== 'string' || !isValidJsonString(data.nodes)) {
|
||||
errors.push('nodes must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.edges !== undefined) {
|
||||
if (typeof data.edges !== 'string' || !isValidJsonString(data.edges)) {
|
||||
errors.push('edges must be a JSON string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.enabled !== undefined && typeof data.enabled !== 'boolean') {
|
||||
errors.push('enabled must be a boolean')
|
||||
}
|
||||
|
||||
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
|
||||
errors.push('version must be a positive integer')
|
||||
}
|
||||
|
||||
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
|
||||
errors.push('createdAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
|
||||
errors.push('updatedAt must be a bigint timestamp')
|
||||
}
|
||||
|
||||
if (data.createdBy !== undefined && data.createdBy !== null) {
|
||||
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
|
||||
errors.push('createdBy must be a non-empty string')
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
|
||||
errors.push('tenantId must be a string')
|
||||
}
|
||||
|
||||
if (data.description !== undefined && typeof data.description !== 'string') {
|
||||
errors.push('description must be a string')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Level validation: 1-5 range
|
||||
// Level validation: 1-6 range
|
||||
export function isValidLevel(level: number): boolean {
|
||||
return Number.isInteger(level) && level >= 1 && level <= 5
|
||||
return Number.isInteger(level) && level >= 1 && level <= 6
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const isValidJsonString = (value: string): boolean => {
|
||||
if (typeof value !== 'string') return false
|
||||
try {
|
||||
JSON.parse(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -13,30 +13,27 @@ interface PackageStyleLoaderProps {
|
||||
packages: string[]
|
||||
}
|
||||
|
||||
export function PackageStyleLoader({ packages }: PackageStyleLoaderProps) {
|
||||
export function PackageStyleLoader({ packages }: PackageStyleLoaderProps): null {
|
||||
useEffect(() => {
|
||||
async function loadStyles() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`📦 Loading styles for ${packages.length} packages...`)
|
||||
|
||||
async function loadStyles(): Promise<void> {
|
||||
const results = await Promise.all(
|
||||
packages.map(async (packageId) => {
|
||||
try {
|
||||
const css = await loadAndInjectStyles(packageId)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`✓ ${packageId} (${css.length} bytes)`)
|
||||
return { packageId, success: true, size: css.length }
|
||||
} catch (error) {
|
||||
console.warn(`✗ ${packageId}:`, error)
|
||||
} catch {
|
||||
return { packageId, success: false, size: 0 }
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const successful = results.filter(r => r.success)
|
||||
const totalSize = successful.reduce((sum, r) => sum + r.size, 0)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`✅ ${successful.length}/${packages.length} packages loaded (${(totalSize / 1024).toFixed(1)}KB)`)
|
||||
// Log summary in development only
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const successful = results.filter(r => r.success)
|
||||
const totalSize = successful.reduce((sum, r) => sum + r.size, 0)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[PackageStyleLoader] ${successful.length}/${packages.length} packages (${(totalSize / 1024).toFixed(1)}KB)`)
|
||||
}
|
||||
}
|
||||
|
||||
if (packages.length > 0) {
|
||||
|
||||
@@ -1,25 +1,58 @@
|
||||
/**
|
||||
* Hook for level-based routing functionality
|
||||
*
|
||||
* Provides permission checking and routing based on the 6-level system:
|
||||
* 0: public, 1: user, 2: moderator, 3: admin, 4: god, 5: supergod
|
||||
*/
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useResolvedUser } from './useResolvedUser'
|
||||
|
||||
export interface LevelRouting {
|
||||
canAccessLevel: (level: number) => boolean
|
||||
redirectToLevel: (level: number) => void
|
||||
/** Check if current user can access a given permission level */
|
||||
canAccessLevel: (requiredLevel: number) => boolean
|
||||
/** Redirect user to an appropriate page for their level */
|
||||
redirectToLevel: (targetLevel: number) => void
|
||||
/** Current user's permission level */
|
||||
currentLevel: number
|
||||
/** Whether the user check is still loading */
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
/** Route mappings for each permission level */
|
||||
const LEVEL_ROUTES: Record<number, string> = {
|
||||
0: '/', // Public home
|
||||
1: '/dashboard', // User dashboard
|
||||
2: '/moderate', // Moderator panel
|
||||
3: '/admin', // Admin panel
|
||||
4: '/god', // God panel
|
||||
5: '/supergod', // Supergod panel
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing level-based routing
|
||||
* TODO: Implement full level routing logic
|
||||
* Uses the resolved user state to check permissions.
|
||||
*/
|
||||
export function useLevelRouting(): LevelRouting {
|
||||
const router = useRouter()
|
||||
const { level, isLoading } = useResolvedUser()
|
||||
|
||||
const canAccessLevel = (requiredLevel: number): boolean => {
|
||||
if (isLoading) {
|
||||
return false // Don't grant access while loading
|
||||
}
|
||||
return level >= requiredLevel
|
||||
}
|
||||
|
||||
const redirectToLevel = (targetLevel: number): void => {
|
||||
const route = LEVEL_ROUTES[targetLevel] ?? LEVEL_ROUTES[0]
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
return {
|
||||
canAccessLevel: (level: number) => {
|
||||
// TODO: Implement level access check
|
||||
return level >= 0
|
||||
},
|
||||
redirectToLevel: (level: number) => {
|
||||
// TODO: Implement redirect logic (suppress console warning for now)
|
||||
void level
|
||||
},
|
||||
canAccessLevel,
|
||||
redirectToLevel,
|
||||
currentLevel: level,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,55 @@
|
||||
/**
|
||||
* Hook for resolved user state
|
||||
*
|
||||
* Provides user information from the auth system with level-based permissions.
|
||||
* Use this hook when you need to check user permissions or identity.
|
||||
*/
|
||||
|
||||
import { useAuth } from '../useAuth'
|
||||
|
||||
export interface ResolvedUserState {
|
||||
userId?: string
|
||||
username?: string
|
||||
level?: number
|
||||
email?: string
|
||||
role?: string
|
||||
level: number
|
||||
tenantId?: string
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing resolved user state
|
||||
* TODO: Implement full user resolution logic
|
||||
* Returns user data from session with computed permission level.
|
||||
*/
|
||||
export function useResolvedUser(): ResolvedUserState {
|
||||
// TODO: Implement user resolution from session/auth
|
||||
const { user, isAuthenticated, isLoading } = useAuth()
|
||||
|
||||
if (isLoading) {
|
||||
return {
|
||||
level: 0,
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (user === null || !isAuthenticated) {
|
||||
return {
|
||||
level: 0,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
level: user.level ?? 0,
|
||||
tenantId: user.tenantId,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* useCodeEditor hook
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
|
||||
export interface EditorFile {
|
||||
path: string
|
||||
@@ -13,6 +13,10 @@ export interface EditorFile {
|
||||
export function useCodeEditor() {
|
||||
const [files, setFiles] = useState<EditorFile[]>([])
|
||||
const [currentFile, setCurrentFile] = useState<EditorFile | null>(null)
|
||||
|
||||
// Use ref to avoid stale closures in callbacks
|
||||
const currentFileRef = useRef<EditorFile | null>(null)
|
||||
currentFileRef.current = currentFile
|
||||
|
||||
const openFile = useCallback((file: EditorFile) => {
|
||||
setFiles(prev => {
|
||||
@@ -29,17 +33,17 @@ export function useCodeEditor() {
|
||||
|
||||
const saveFile = useCallback((file: EditorFile) => {
|
||||
setFiles(prev => prev.map(f => f.path === file.path ? file : f))
|
||||
if (currentFile?.path === file.path) {
|
||||
if (currentFileRef.current?.path === file.path) {
|
||||
setCurrentFile(file)
|
||||
}
|
||||
}, [currentFile])
|
||||
}, [])
|
||||
|
||||
const closeFile = useCallback((path: string) => {
|
||||
setFiles(prev => prev.filter(f => f.path !== path))
|
||||
if (currentFile?.path === path) {
|
||||
if (currentFileRef.current?.path === path) {
|
||||
setCurrentFile(null)
|
||||
}
|
||||
}, [currentFile])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
files,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { seedAppConfig } from './app/seed-app-config'
|
||||
import { seedCssCategories } from './css/seed-css-categories'
|
||||
import { seedDropdownConfigs } from './dropdowns/seed-dropdown-configs'
|
||||
import { seedUsers } from './users/seed-users'
|
||||
|
||||
/**
|
||||
* Seed database with default data
|
||||
*/
|
||||
export const seedDefaultData = async (): Promise<void> => {
|
||||
// TODO: Implement seedUsers function and import it
|
||||
// await seedUsers()
|
||||
await seedUsers()
|
||||
await seedAppConfig()
|
||||
await seedCssCategories()
|
||||
await seedDropdownConfigs()
|
||||
}
|
||||
|
||||
export const defaultDataBuilders = {
|
||||
// seedUsers,
|
||||
seedUsers,
|
||||
seedAppConfig,
|
||||
seedCssCategories,
|
||||
seedDropdownConfigs,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { getAdapter } from '../../../core/dbal-client'
|
||||
import { hashPassword } from '../../../password/hash-password'
|
||||
|
||||
/**
|
||||
* Default users for initial system setup.
|
||||
* In production, change these passwords immediately after first login.
|
||||
*/
|
||||
const DEFAULT_USERS = [
|
||||
{
|
||||
id: 'user_supergod',
|
||||
username: 'admin',
|
||||
email: 'admin@localhost',
|
||||
role: 'supergod',
|
||||
isInstanceOwner: true,
|
||||
password: 'admin123', // Change immediately in production!
|
||||
},
|
||||
{
|
||||
id: 'user_god',
|
||||
username: 'god',
|
||||
email: 'god@localhost',
|
||||
role: 'god',
|
||||
isInstanceOwner: false,
|
||||
password: 'god123',
|
||||
},
|
||||
{
|
||||
id: 'user_admin',
|
||||
username: 'manager',
|
||||
email: 'manager@localhost',
|
||||
role: 'admin',
|
||||
isInstanceOwner: false,
|
||||
password: 'manager123',
|
||||
},
|
||||
{
|
||||
id: 'user_demo',
|
||||
username: 'demo',
|
||||
email: 'demo@localhost',
|
||||
role: 'user',
|
||||
isInstanceOwner: false,
|
||||
password: 'demo123',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Seed default users and their credentials.
|
||||
* Creates users only if they don't already exist.
|
||||
*/
|
||||
export async function seedUsers(): Promise<void> {
|
||||
const adapter = getAdapter()
|
||||
const now = BigInt(Date.now())
|
||||
|
||||
for (const userData of DEFAULT_USERS) {
|
||||
// Check if user already exists
|
||||
const existing = await adapter.findFirst('User', {
|
||||
where: { username: userData.username },
|
||||
})
|
||||
|
||||
if (existing !== null && existing !== undefined) {
|
||||
// User already exists, skip
|
||||
continue
|
||||
}
|
||||
|
||||
// Create user
|
||||
await adapter.create('User', {
|
||||
id: userData.id,
|
||||
username: userData.username,
|
||||
email: userData.email,
|
||||
role: userData.role,
|
||||
isInstanceOwner: userData.isInstanceOwner,
|
||||
createdAt: now,
|
||||
tenantId: null,
|
||||
profilePicture: null,
|
||||
bio: null,
|
||||
})
|
||||
|
||||
// Create credential for login
|
||||
const passwordHash = await hashPassword(userData.password)
|
||||
await adapter.create('Credential', {
|
||||
id: `cred_${userData.id}`,
|
||||
username: userData.username,
|
||||
passwordHash,
|
||||
userId: userData.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ interface RequestOptions {
|
||||
orderBy?: Record<string, 'asc' | 'desc'>
|
||||
/** Override the package for this request (useful for dependency packages) */
|
||||
packageId?: string
|
||||
/** AbortSignal for cancelling the request */
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,9 +106,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { packageId: pkgOverride, ...queryOpts } = options ?? {}
|
||||
const { packageId: pkgOverride, signal, ...queryOpts } = options ?? {}
|
||||
const url = buildUrl(entity, undefined, undefined, pkgOverride) + buildQueryString(queryOpts as RequestOptions)
|
||||
const response = await fetch(url)
|
||||
const response = await fetch(url, { signal })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
@@ -120,6 +122,10 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
|
||||
return json.data ?? []
|
||||
} catch (err) {
|
||||
// Don't set error state for aborted requests
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(message)
|
||||
throw err
|
||||
@@ -134,13 +140,13 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
* Read single entity
|
||||
*/
|
||||
const read = useCallback(
|
||||
async (entity: string, id: string): Promise<T | null> => {
|
||||
async (entity: string, id: string, options?: { signal?: AbortSignal }): Promise<T | null> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const url = buildUrl(entity, id)
|
||||
const response = await fetch(url)
|
||||
const response = await fetch(url, { signal: options?.signal })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
@@ -154,6 +160,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
|
||||
return json.data ?? null
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(message)
|
||||
throw err
|
||||
@@ -168,7 +177,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
* Create entity
|
||||
*/
|
||||
const create = useCallback(
|
||||
async (entity: string, data: Record<string, unknown>): Promise<T> => {
|
||||
async (entity: string, data: Record<string, unknown>, options?: { signal?: AbortSignal }): Promise<T> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
@@ -178,6 +187,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
signal: options?.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -192,6 +202,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
|
||||
return json.data as T
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(message)
|
||||
throw err
|
||||
@@ -206,7 +219,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
* Update entity
|
||||
*/
|
||||
const update = useCallback(
|
||||
async (entity: string, id: string, data: Record<string, unknown>): Promise<T> => {
|
||||
async (entity: string, id: string, data: Record<string, unknown>, options?: { signal?: AbortSignal }): Promise<T> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
@@ -216,6 +229,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
signal: options?.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -230,6 +244,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
|
||||
return json.data as T
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(message)
|
||||
throw err
|
||||
@@ -244,13 +261,13 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
* Delete entity
|
||||
*/
|
||||
const remove = useCallback(
|
||||
async (entity: string, id: string): Promise<void> => {
|
||||
async (entity: string, id: string, options?: { signal?: AbortSignal }): Promise<void> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const url = buildUrl(entity, id)
|
||||
const response = await fetch(url, { method: 'DELETE' })
|
||||
const response = await fetch(url, { method: 'DELETE', signal: options?.signal })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
@@ -262,6 +279,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
throw new Error(json.error ?? 'Request failed')
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(message)
|
||||
throw err
|
||||
@@ -280,7 +300,8 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
entity: string,
|
||||
id: string,
|
||||
actionName: string,
|
||||
data?: Record<string, unknown>
|
||||
data?: Record<string, unknown>,
|
||||
options?: { signal?: AbortSignal }
|
||||
): Promise<T> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -291,6 +312,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: data !== undefined ? JSON.stringify(data) : undefined,
|
||||
signal: options?.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -305,6 +327,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
|
||||
|
||||
return json.data as T
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setError(message)
|
||||
throw err
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Generate component tree from Lua (stub)
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface ComponentTree {
|
||||
type: string
|
||||
props?: Record<string, unknown>
|
||||
children?: ComponentTree[]
|
||||
}
|
||||
|
||||
export function generateComponentTree(_luaScript: unknown): ReactNode {
|
||||
// TODO: Implement Lua component tree generation
|
||||
return null
|
||||
}
|
||||
100
frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx
Normal file
100
frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Generate React component tree from Lua UI component definitions
|
||||
*
|
||||
* Transforms LuaUIComponent structures into React elements using
|
||||
* the JSON component renderer infrastructure.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from 'react'
|
||||
import type { LuaUIComponent } from './types/lua-ui-package'
|
||||
|
||||
export interface ComponentTree {
|
||||
type: string
|
||||
props?: Record<string, unknown>
|
||||
children?: ComponentTree[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of basic HTML elements that can be rendered directly
|
||||
*/
|
||||
const HTML_ELEMENTS = new Set([
|
||||
'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'section', 'article', 'header', 'footer', 'main', 'aside', 'nav',
|
||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
||||
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td',
|
||||
'form', 'input', 'button', 'select', 'option', 'textarea', 'label',
|
||||
'a', 'img', 'video', 'audio', 'canvas', 'svg',
|
||||
'strong', 'em', 'code', 'pre', 'blockquote',
|
||||
])
|
||||
|
||||
/**
|
||||
* Render a single LuaUIComponent to a React element
|
||||
*/
|
||||
function renderComponent(component: LuaUIComponent, key?: string | number): ReactNode {
|
||||
const { type, props = {}, children } = component
|
||||
|
||||
// Render children recursively
|
||||
const renderedChildren = children?.map((child, index) =>
|
||||
renderComponent(child, index)
|
||||
)
|
||||
|
||||
// Handle text content
|
||||
if (type === 'text' && typeof props.content === 'string') {
|
||||
return props.content
|
||||
}
|
||||
|
||||
// Handle HTML elements
|
||||
if (HTML_ELEMENTS.has(type)) {
|
||||
return React.createElement(
|
||||
type,
|
||||
{ ...props, key },
|
||||
renderedChildren
|
||||
)
|
||||
}
|
||||
|
||||
// Handle custom components - wrap in div with data attribute for future component registry
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
'data-component': type,
|
||||
className: `component-${type}`,
|
||||
...props,
|
||||
key,
|
||||
},
|
||||
renderedChildren ?? (
|
||||
<span className="component-placeholder">
|
||||
[{type}]
|
||||
</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete React component tree from a Lua UI component definition.
|
||||
*
|
||||
* @param luaComponent - The root LuaUIComponent to render
|
||||
* @returns React node tree, or null if input is invalid
|
||||
*/
|
||||
export function generateComponentTree(luaComponent: unknown): ReactNode {
|
||||
// Validate input
|
||||
if (luaComponent === null || luaComponent === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof luaComponent !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const component = luaComponent as LuaUIComponent
|
||||
|
||||
// Must have a type to render
|
||||
if (typeof component.type !== 'string' || component.type.length === 0) {
|
||||
return (
|
||||
<div className="component-error">
|
||||
Invalid component: missing type
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return renderComponent(component)
|
||||
}
|
||||
Reference in New Issue
Block a user