diff --git a/dbal/development/src/core/entities/operations/system/component-operations.ts b/dbal/development/src/core/entities/operations/system/component-operations.ts index 382d80f08..a95b8d0c2 100644 --- a/dbal/development/src/core/entities/operations/system/component-operations.ts +++ b/dbal/development/src/core/entities/operations/system/component-operations.ts @@ -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 - read: (id: string) => Promise - update: (id: string, data: Partial) => Promise +export interface ComponentNodeOperations { + create: (data: CreateComponentNodeInput) => Promise + read: (id: string) => Promise + update: (id: string, data: UpdateComponentNodeInput) => Promise delete: (id: string) => Promise - getTree: (pageId: string) => Promise + getTree: (pageId: string) => Promise } -type ComponentCreatePayload = Omit & { 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) => { +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): 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 { - 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 + const payload = withComponentDefaults(data) + return adapter.create('ComponentNode', payload) as Promise }, 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 + await assertPageTenant(adapter, resolvedTenantId, existing.pageId) + if (data.pageId) { + await assertPageTenant(adapter, resolvedTenantId, data.pageId) + } + return adapter.update('ComponentNode', id, data) as Promise }, 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 + }) as ListResult return result.data }, }) + +export const createComponentOperations = createComponentNodeOperations diff --git a/dbal/development/src/core/entities/operations/system/package/batch.ts b/dbal/development/src/core/entities/operations/system/package/batch.ts index d8ed71426..a25d80a93 100644 --- a/dbal/development/src/core/entities/operations/system/package/batch.ts +++ b/dbal/development/src/core/entities/operations/system/package/batch.ts @@ -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>, + data: InstalledPackage[], ): Promise => { 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[]) + return adapter.createMany('InstalledPackage', data as Record[]) } 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, - data: Partial, + data: Partial, ): Promise => { 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) + return adapter.updateMany('InstalledPackage', filter, data as Record) } 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): Promise => { +export const deleteManyInstalledPackages = async ( + adapter: DBALAdapter, + filter: Record, +): Promise => { 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) } diff --git a/dbal/development/src/core/entities/operations/system/package/mutations.ts b/dbal/development/src/core/entities/operations/system/package/mutations.ts index dd7b5647f..7511f9321 100644 --- a/dbal/development/src/core/entities/operations/system/package/mutations.ts +++ b/dbal/development/src/core/entities/operations/system/package/mutations.ts @@ -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, -): Promise => { + data: InstalledPackage, +): Promise => { 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 + return adapter.create('InstalledPackage', data) as Promise } 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, -): Promise => { - const idErrors = validateId(id) + packageId: string, + data: Partial, +): Promise => { + 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 + return adapter.update('InstalledPackage', packageId, data) as Promise } 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 => { - const validationErrors = validateId(id) +export const deleteInstalledPackage = async ( + adapter: DBALAdapter, + packageId: string, +): Promise => { + 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 } diff --git a/dbal/development/src/core/entities/operations/system/package/publish.ts b/dbal/development/src/core/entities/operations/system/package/publish.ts index 024a3134b..63bca932b 100644 --- a/dbal/development/src/core/entities/operations/system/package/publish.ts +++ b/dbal/development/src/core/entities/operations/system/package/publish.ts @@ -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, -): Promise => { - return createPackage(adapter, data) + data: InstalledPackage, +): Promise => { + return createInstalledPackage(adapter, data) } diff --git a/dbal/development/src/core/entities/operations/system/package/reads.ts b/dbal/development/src/core/entities/operations/system/package/reads.ts index b44003ea0..758b36b06 100644 --- a/dbal/development/src/core/entities/operations/system/package/reads.ts +++ b/dbal/development/src/core/entities/operations/system/package/reads.ts @@ -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 => { - const validationErrors = validateId(id) +export const readInstalledPackage = async ( + adapter: DBALAdapter, + packageId: string, +): Promise => { + 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> => { - return adapter.list('Package', options) as Promise> +export const listInstalledPackages = ( + adapter: DBALAdapter, + options?: ListOptions, +): Promise> => { + return adapter.list('InstalledPackage', options) as Promise> } diff --git a/dbal/development/src/core/entities/operations/system/package/unpublish.ts b/dbal/development/src/core/entities/operations/system/package/unpublish.ts index e6ff44a04..5acadd000 100644 --- a/dbal/development/src/core/entities/operations/system/package/unpublish.ts +++ b/dbal/development/src/core/entities/operations/system/package/unpublish.ts @@ -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 => { - return deletePackage(adapter, id) + return deleteInstalledPackage(adapter, id) } diff --git a/dbal/development/src/core/entities/operations/system/package/validate.ts b/dbal/development/src/core/entities/operations/system/package/validate.ts index 868033e9e..b37428301 100644 --- a/dbal/development/src/core/entities/operations/system/package/validate.ts +++ b/dbal/development/src/core/entities/operations/system/package/validate.ts @@ -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): string[] => { +export const validatePackage = (data: Partial): string[] => { return validatePackageCreate(data) } diff --git a/dbal/development/src/core/entities/operations/system/page-operations.ts b/dbal/development/src/core/entities/operations/system/page-operations.ts index c83ef7069..cdd141c9a 100644 --- a/dbal/development/src/core/entities/operations/system/page-operations.ts +++ b/dbal/development/src/core/entities/operations/system/page-operations.ts @@ -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 - read: (id: string) => Promise - readBySlug: (slug: string) => Promise - update: (id: string, data: Partial) => Promise +export interface PageConfigOperations { + create: (data: CreatePageInput) => Promise + read: (id: string) => Promise + readByPath: (path: string) => Promise + update: (id: string, data: UpdatePageInput) => Promise delete: (id: string) => Promise - list: (options?: ListOptions) => Promise> + list: (options?: ListOptions) => Promise> } -type PageCreatePayload = Omit & { 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) => { +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): string | null => { +const resolveTenantId = (configuredTenantId?: string, data?: Partial): 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 + return adapter.create('PageConfig', payload) as Promise } 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 + return adapter.update('PageConfig', id, data) as Promise } 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> + return adapter.list('PageConfig', { ...options, filter: tenantFilter }) as Promise> }, }) + +export const createPageOperations = createPageConfigOperations diff --git a/dbal/development/src/core/foundation/types/auth/index.ts b/dbal/development/src/core/foundation/types/auth/index.ts index 3dcbd509e..8a6ec7ef1 100644 --- a/dbal/development/src/core/foundation/types/auth/index.ts +++ b/dbal/development/src/core/foundation/types/auth/index.ts @@ -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 } diff --git a/dbal/development/src/core/foundation/types/automation/index.ts b/dbal/development/src/core/foundation/types/automation/index.ts index 8091e8c4b..333ab2f8d 100644 --- a/dbal/development/src/core/foundation/types/automation/index.ts +++ b/dbal/development/src/core/foundation/types/automation/index.ts @@ -1,71 +1,90 @@ export interface Workflow { id: string - tenantId: string name: string description?: string - trigger: 'manual' | 'schedule' | 'event' | 'webhook' - triggerConfig: Record - steps: Record - 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 - steps: Record - 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 - steps?: Record - 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 } diff --git a/dbal/development/src/core/foundation/types/content/index.ts b/dbal/development/src/core/foundation/types/content/index.ts index 567f1c3de..2984eb257 100644 --- a/dbal/development/src/core/foundation/types/content/index.ts +++ b/dbal/development/src/core/foundation/types/content/index.ts @@ -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 - 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 - 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 - 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 - 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 } diff --git a/dbal/development/src/core/foundation/types/entities.ts b/dbal/development/src/core/foundation/types/entities.ts index dcd20b271..0e1f37b6c 100644 --- a/dbal/development/src/core/foundation/types/entities.ts +++ b/dbal/development/src/core/foundation/types/entities.ts @@ -11,7 +11,7 @@ export interface SoftDeletableEntity extends BaseEntity { } export interface TenantScopedEntity extends BaseEntity { - tenantId: string + tenantId?: string | null } export interface EntityMetadata { diff --git a/dbal/development/src/core/foundation/types/packages/index.ts b/dbal/development/src/core/foundation/types/packages/index.ts index ff1be4cdd..276c4badd 100644 --- a/dbal/development/src/core/foundation/types/packages/index.ts +++ b/dbal/development/src/core/foundation/types/packages/index.ts @@ -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 - 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 - 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 - 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 } diff --git a/dbal/development/src/core/foundation/types/users/index.ts b/dbal/development/src/core/foundation/types/users/index.ts index 4aeec0230..cba2a032f 100644 --- a/dbal/development/src/core/foundation/types/users/index.ts +++ b/dbal/development/src/core/foundation/types/users/index.ts @@ -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 } diff --git a/dbal/development/src/core/foundation/validation.ts b/dbal/development/src/core/foundation/validation.ts index 331a149ea..46f8fb2f2 100644 --- a/dbal/development/src/core/foundation/validation.ts +++ b/dbal/development/src/core/foundation/validation.ts @@ -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' diff --git a/dbal/development/src/core/foundation/validation/is-valid-json.ts b/dbal/development/src/core/foundation/validation/is-valid-json.ts new file mode 100644 index 000000000..f875ceb9a --- /dev/null +++ b/dbal/development/src/core/foundation/validation/is-valid-json.ts @@ -0,0 +1,3 @@ +import { isValidJsonString as isValidJsonStringPredicate } from '../../validation/predicates/string/is-valid-json' + +export const isValidJsonString = (value: string): boolean => isValidJsonStringPredicate(value) diff --git a/dbal/development/src/core/types.ts b/dbal/development/src/core/types.ts index 7caaee020..8d02ecb5a 100644 --- a/dbal/development/src/core/types.ts +++ b/dbal/development/src/core/types.ts @@ -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 { - success: boolean; - data?: T; - error?: DBALError; + success: boolean + data?: T + error?: DBALError } export interface ListOptions { - filter?: Record; - sort?: Record; - page?: number; - limit?: number; + filter?: Record + sort?: Record + page?: number + limit?: number } export interface ListResult { - 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' diff --git a/dbal/development/src/core/validation/entities/component/validate-component-hierarchy-create.ts b/dbal/development/src/core/validation/entities/component/validate-component-hierarchy-create.ts index 6edb2309e..15360c696 100644 --- a/dbal/development/src/core/validation/entities/component/validate-component-hierarchy-create.ts +++ b/dbal/development/src/core/validation/entities/component/validate-component-hierarchy-create.ts @@ -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): string[] { +export function validateComponentHierarchyCreate(data: Partial): 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): string[] { +export function validateComponentHierarchyUpdate(data: Partial): 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): 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 } diff --git a/dbal/development/src/core/validation/entities/credential/validate-credential-update.ts b/dbal/development/src/core/validation/entities/credential/validate-credential-update.ts index 6df66a285..cd50ac260 100644 --- a/dbal/development/src/core/validation/entities/credential/validate-credential-update.ts +++ b/dbal/development/src/core/validation/entities/credential/validate-credential-update.ts @@ -16,9 +16,5 @@ export function validateCredentialUpdate(data: Partial): string[] { } } - if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') { - errors.push('firstLogin must be a boolean') - } - return errors } diff --git a/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-create.ts b/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-create.ts index 87f9553a6..9825990fa 100644 --- a/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-create.ts +++ b/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-create.ts @@ -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): string[] { const errors: string[] = [] @@ -17,6 +17,16 @@ export function validateLuaScriptCreate(data: Partial): 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): 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): 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') { diff --git a/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-update.ts b/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-update.ts index acb965e07..68acfd152 100644 --- a/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-update.ts +++ b/dbal/development/src/core/validation/entities/lua-script/validate-lua-script-update.ts @@ -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): string[] { const errors: string[] = [] @@ -17,19 +17,38 @@ export function validateLuaScriptUpdate(data: Partial): 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): 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') } diff --git a/dbal/development/src/core/validation/entities/package/validate-package-create.ts b/dbal/development/src/core/validation/entities/package/validate-package-create.ts index 9206f95c7..6cf52fda4 100644 --- a/dbal/development/src/core/validation/entities/package/validate-package-create.ts +++ b/dbal/development/src/core/validation/entities/package/validate-package-create.ts @@ -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): string[] { +export function validatePackageCreate(data: Partial): 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): 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 diff --git a/dbal/development/src/core/validation/entities/package/validate-package-update.ts b/dbal/development/src/core/validation/entities/package/validate-package-update.ts index b30bcc253..45b6328c1 100644 --- a/dbal/development/src/core/validation/entities/package/validate-package-update.ts +++ b/dbal/development/src/core/validation/entities/package/validate-package-update.ts @@ -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): string[] { +export function validatePackageUpdate(data: Partial): 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): 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 diff --git a/dbal/development/src/core/validation/entities/page/validate-page-create.ts b/dbal/development/src/core/validation/entities/page/validate-page-create.ts index da3df5e92..612d76bba 100644 --- a/dbal/development/src/core/validation/entities/page/validate-page-create.ts +++ b/dbal/development/src/core/validation/entities/page/validate-page-create.ts @@ -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): string[] { +export function validatePageCreate(data: Partial): 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): 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 } diff --git a/dbal/development/src/core/validation/entities/page/validate-page-update.ts b/dbal/development/src/core/validation/entities/page/validate-page-update.ts index dbb03c723..23b4a1fce 100644 --- a/dbal/development/src/core/validation/entities/page/validate-page-update.ts +++ b/dbal/development/src/core/validation/entities/page/validate-page-update.ts @@ -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): string[] { +export function validatePageUpdate(data: Partial): 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 } diff --git a/dbal/development/src/core/validation/entities/session/validate-session-create.ts b/dbal/development/src/core/validation/entities/session/validate-session-create.ts index 6155ffa2a..6d14ae1a8 100644 --- a/dbal/development/src/core/validation/entities/session/validate-session-create.ts +++ b/dbal/development/src/core/validation/entities/session/validate-session-create.ts @@ -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): 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): 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 diff --git a/dbal/development/src/core/validation/entities/session/validate-session-update.ts b/dbal/development/src/core/validation/entities/session/validate-session-update.ts index 6e51c0364..9d86c3f12 100644 --- a/dbal/development/src/core/validation/entities/session/validate-session-update.ts +++ b/dbal/development/src/core/validation/entities/session/validate-session-update.ts @@ -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): 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): 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 diff --git a/dbal/development/src/core/validation/entities/types.ts b/dbal/development/src/core/validation/entities/types.ts index b1e34c97e..631385e6c 100644 --- a/dbal/development/src/core/validation/entities/types.ts +++ b/dbal/development/src/core/validation/entities/types.ts @@ -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 } diff --git a/dbal/development/src/core/validation/entities/user/validate-user-create.ts b/dbal/development/src/core/validation/entities/user/validate-user-create.ts index 1509a0f6e..189bcaef0 100644 --- a/dbal/development/src/core/validation/entities/user/validate-user-create.ts +++ b/dbal/development/src/core/validation/entities/user/validate-user-create.ts @@ -22,9 +22,37 @@ export function validateUserCreate(data: Partial): 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 } diff --git a/dbal/development/src/core/validation/entities/user/validate-user-update.ts b/dbal/development/src/core/validation/entities/user/validate-user-update.ts index 59061ae9d..29765ce17 100644 --- a/dbal/development/src/core/validation/entities/user/validate-user-update.ts +++ b/dbal/development/src/core/validation/entities/user/validate-user-update.ts @@ -13,9 +13,37 @@ export function validateUserUpdate(data: Partial): 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 } diff --git a/dbal/development/src/core/validation/entities/validate-id.ts b/dbal/development/src/core/validation/entities/validate-id.ts index b73b2a65d..23953f7c2 100644 --- a/dbal/development/src/core/validation/entities/validate-id.ts +++ b/dbal/development/src/core/validation/entities/validate-id.ts @@ -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 } diff --git a/dbal/development/src/core/validation/entities/workflow/validate-workflow-create.ts b/dbal/development/src/core/validation/entities/workflow/validate-workflow-create.ts index 6bca941b8..0c34ae81c 100644 --- a/dbal/development/src/core/validation/entities/workflow/validate-workflow-create.ts +++ b/dbal/development/src/core/validation/entities/workflow/validate-workflow-create.ts @@ -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): string[] { const errors: string[] = [] @@ -13,34 +10,44 @@ export function validateWorkflowCreate(data: Partial): 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') { diff --git a/dbal/development/src/core/validation/entities/workflow/validate-workflow-update.ts b/dbal/development/src/core/validation/entities/workflow/validate-workflow-update.ts index 76357ad13..3ccbf6681 100644 --- a/dbal/development/src/core/validation/entities/workflow/validate-workflow-update.ts +++ b/dbal/development/src/core/validation/entities/workflow/validate-workflow-update.ts @@ -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): string[] { const errors: string[] = [] @@ -13,28 +10,44 @@ export function validateWorkflowUpdate(data: Partial): 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') } diff --git a/dbal/development/src/core/validation/predicates/is-valid-level.ts b/dbal/development/src/core/validation/predicates/is-valid-level.ts index eef16b9c7..579d3f188 100644 --- a/dbal/development/src/core/validation/predicates/is-valid-level.ts +++ b/dbal/development/src/core/validation/predicates/is-valid-level.ts @@ -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 } diff --git a/dbal/development/src/core/validation/predicates/string/is-valid-json.ts b/dbal/development/src/core/validation/predicates/string/is-valid-json.ts new file mode 100644 index 000000000..5e1a1dd45 --- /dev/null +++ b/dbal/development/src/core/validation/predicates/string/is-valid-json.ts @@ -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 + } +} diff --git a/frontends/nextjs/src/components/PackageStyleLoader.tsx b/frontends/nextjs/src/components/PackageStyleLoader.tsx index 5c80bb6c7..688b928f4 100644 --- a/frontends/nextjs/src/components/PackageStyleLoader.tsx +++ b/frontends/nextjs/src/components/PackageStyleLoader.tsx @@ -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 { 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) { diff --git a/frontends/nextjs/src/hooks/data/useLevelRouting.ts b/frontends/nextjs/src/hooks/data/useLevelRouting.ts index a122324db..dd5f483a4 100644 --- a/frontends/nextjs/src/hooks/data/useLevelRouting.ts +++ b/frontends/nextjs/src/hooks/data/useLevelRouting.ts @@ -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 = { + 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, } } diff --git a/frontends/nextjs/src/hooks/data/useResolvedUser.ts b/frontends/nextjs/src/hooks/data/useResolvedUser.ts index 8c4b78a9b..a169a0475 100644 --- a/frontends/nextjs/src/hooks/data/useResolvedUser.ts +++ b/frontends/nextjs/src/hooks/data/useResolvedUser.ts @@ -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, } } diff --git a/frontends/nextjs/src/hooks/useCodeEditor.ts b/frontends/nextjs/src/hooks/useCodeEditor.ts index 78a940c26..d1ed62894 100644 --- a/frontends/nextjs/src/hooks/useCodeEditor.ts +++ b/frontends/nextjs/src/hooks/useCodeEditor.ts @@ -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([]) const [currentFile, setCurrentFile] = useState(null) + + // Use ref to avoid stale closures in callbacks + const currentFileRef = useRef(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, diff --git a/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts b/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts index f540c063c..59d1c726c 100644 --- a/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts +++ b/frontends/nextjs/src/lib/db/database-admin/seed-default-data/index.ts @@ -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 => { - // 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, diff --git a/frontends/nextjs/src/lib/db/database-admin/seed-default-data/users/seed-users.ts b/frontends/nextjs/src/lib/db/database-admin/seed-default-data/users/seed-users.ts new file mode 100644 index 000000000..ffe1886ba --- /dev/null +++ b/frontends/nextjs/src/lib/db/database-admin/seed-default-data/users/seed-users.ts @@ -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 { + 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, + }) + } +} diff --git a/frontends/nextjs/src/lib/hooks/use-rest-api.ts b/frontends/nextjs/src/lib/hooks/use-rest-api.ts index 3d8a7e60d..f595431ff 100644 --- a/frontends/nextjs/src/lib/hooks/use-rest-api.ts +++ b/frontends/nextjs/src/lib/hooks/use-rest-api.ts @@ -32,6 +32,8 @@ interface RequestOptions { orderBy?: Record /** 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(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(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(options?: UseRestApiOptions) { * Read single entity */ const read = useCallback( - async (entity: string, id: string): Promise => { + async (entity: string, id: string, options?: { signal?: AbortSignal }): Promise => { 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(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(options?: UseRestApiOptions) { * Create entity */ const create = useCallback( - async (entity: string, data: Record): Promise => { + async (entity: string, data: Record, options?: { signal?: AbortSignal }): Promise => { setLoading(true) setError(null) @@ -178,6 +187,7 @@ export function useRestApi(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(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(options?: UseRestApiOptions) { * Update entity */ const update = useCallback( - async (entity: string, id: string, data: Record): Promise => { + async (entity: string, id: string, data: Record, options?: { signal?: AbortSignal }): Promise => { setLoading(true) setError(null) @@ -216,6 +229,7 @@ export function useRestApi(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(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(options?: UseRestApiOptions) { * Delete entity */ const remove = useCallback( - async (entity: string, id: string): Promise => { + async (entity: string, id: string, options?: { signal?: AbortSignal }): Promise => { 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(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(options?: UseRestApiOptions) { entity: string, id: string, actionName: string, - data?: Record + data?: Record, + options?: { signal?: AbortSignal } ): Promise => { setLoading(true) setError(null) @@ -291,6 +312,7 @@ export function useRestApi(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(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 diff --git a/frontends/nextjs/src/lib/lua/ui/generate-component-tree.ts b/frontends/nextjs/src/lib/lua/ui/generate-component-tree.ts deleted file mode 100644 index 3c19f7c20..000000000 --- a/frontends/nextjs/src/lib/lua/ui/generate-component-tree.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generate component tree from Lua (stub) - */ - -import type { ReactNode } from 'react' - -export interface ComponentTree { - type: string - props?: Record - children?: ComponentTree[] -} - -export function generateComponentTree(_luaScript: unknown): ReactNode { - // TODO: Implement Lua component tree generation - return null -} diff --git a/frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx b/frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx new file mode 100644 index 000000000..19154cd78 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx @@ -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 + 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 ?? ( + + [{type}] + + ) + ) +} + +/** + * 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 ( +
+ Invalid component: missing type +
+ ) + } + + return renderComponent(component) +}