mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Refactor workflow management and validation
- Updated `list_workflows.hpp` to replace `is_active` with `enabled` filter and added `tenant_id` filter. - Modified sorting logic for workflows based on `created_at` to handle optional values. - Enhanced `update_workflow.hpp` to include new fields: `nodes`, `edges`, `enabled`, `version`, and `created_at`. - Removed obsolete `isValidWorkflowTrigger` validation function from `workflow_validation.hpp`. - Adjusted unit tests in `client_test.cpp` to reflect changes in workflow and page management, including renaming fields and updating assertions. - Updated Prisma schema in `schema.prisma` to align with new workflow and component models, including changes to data types and attributes. - Introduced a new script `gen_prisma_schema.js` for generating Prisma schema from DBAL API schemas to maintain synchronization.
This commit is contained in:
246
dbal/development/src/adapters/memory/index.ts
Normal file
246
dbal/development/src/adapters/memory/index.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import type { AdapterCapabilities, DBALAdapter } from '../adapter'
|
||||
import type { ListOptions, ListResult } from '../../core/foundation/types'
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
|
||||
const ID_FIELDS: Record<string, string> = {
|
||||
Credential: 'username',
|
||||
InstalledPackage: 'packageId',
|
||||
PackageData: 'packageId',
|
||||
}
|
||||
|
||||
const resolveIdField = (entity: string, data?: Record<string, unknown>): string => {
|
||||
if (ID_FIELDS[entity]) {
|
||||
return ID_FIELDS[entity]
|
||||
}
|
||||
if (data && typeof data.id === 'string' && data.id.trim().length > 0) {
|
||||
return 'id'
|
||||
}
|
||||
return 'id'
|
||||
}
|
||||
|
||||
const getRecordId = (entity: string, data: Record<string, unknown>): string => {
|
||||
const idField = resolveIdField(entity, data)
|
||||
const value = data[idField]
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
throw DBALError.validationError(`${entity} ${idField} is required`, [
|
||||
{ field: idField, error: `${idField} is required` },
|
||||
])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const applyFilter = (
|
||||
records: Record<string, unknown>[],
|
||||
filter?: Record<string, unknown>,
|
||||
): Record<string, unknown>[] => {
|
||||
if (!filter || Object.keys(filter).length === 0) {
|
||||
return records
|
||||
}
|
||||
return records.filter((record) =>
|
||||
Object.entries(filter).every(([key, value]) => record[key] === value),
|
||||
)
|
||||
}
|
||||
|
||||
const applySort = (
|
||||
records: Record<string, unknown>[],
|
||||
sort?: Record<string, 'asc' | 'desc'>,
|
||||
): Record<string, unknown>[] => {
|
||||
if (!sort || Object.keys(sort).length === 0) {
|
||||
return records
|
||||
}
|
||||
const [key, direction] = Object.entries(sort)[0]
|
||||
return [...records].sort((left, right) => {
|
||||
const a = left[key]
|
||||
const b = right[key]
|
||||
if (typeof a === 'string' && typeof b === 'string') {
|
||||
return direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a)
|
||||
}
|
||||
if (typeof a === 'number' && typeof b === 'number') {
|
||||
return direction === 'asc' ? a - b : b - a
|
||||
}
|
||||
if (typeof a === 'bigint' && typeof b === 'bigint') {
|
||||
return direction === 'asc' ? Number(a - b) : Number(b - a)
|
||||
}
|
||||
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
||||
return direction === 'asc' ? Number(a) - Number(b) : Number(b) - Number(a)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
export class MemoryAdapter implements DBALAdapter {
|
||||
private store: Map<string, Map<string, Record<string, unknown>>> = new Map()
|
||||
|
||||
private getEntityStore(entity: string): Map<string, Record<string, unknown>> {
|
||||
const existing = this.store.get(entity)
|
||||
if (existing) return existing
|
||||
const created = new Map<string, Record<string, unknown>>()
|
||||
this.store.set(entity, created)
|
||||
return created
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const id = getRecordId(entity, data)
|
||||
if (entityStore.has(id)) {
|
||||
throw DBALError.conflict(`${entity} already exists: ${id}`)
|
||||
}
|
||||
const record = { ...data }
|
||||
entityStore.set(id, record)
|
||||
return record
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
return entityStore.get(id) ?? null
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const existing = entityStore.get(id)
|
||||
if (!existing) {
|
||||
throw DBALError.notFound(`${entity} not found: ${id}`)
|
||||
}
|
||||
const record = { ...existing, ...data }
|
||||
entityStore.set(id, record)
|
||||
return record
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
return entityStore.delete(id)
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const page = options?.page ?? 1
|
||||
const limit = options?.limit ?? 20
|
||||
const filtered = applyFilter(Array.from(entityStore.values()), options?.filter)
|
||||
const sorted = applySort(filtered, options?.sort)
|
||||
const start = (page - 1) * limit
|
||||
const data = sorted.slice(start, start + limit)
|
||||
return {
|
||||
data,
|
||||
total: filtered.length,
|
||||
page,
|
||||
limit,
|
||||
hasMore: start + limit < filtered.length,
|
||||
}
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const result = applyFilter(Array.from(entityStore.values()), filter)
|
||||
return result[0] ?? null
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
return this.findFirst(entity, { [field]: value })
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const existing = Array.from(entityStore.entries()).find(([, record]) => record[uniqueField] === uniqueValue)
|
||||
if (existing) {
|
||||
const [id, record] = existing
|
||||
const next = { ...record, ...updateData }
|
||||
entityStore.set(id, next)
|
||||
return next
|
||||
}
|
||||
const payload = { ...createData, [uniqueField]: uniqueValue }
|
||||
return this.create(entity, payload)
|
||||
}
|
||||
|
||||
async updateByField(
|
||||
entity: string,
|
||||
field: string,
|
||||
value: unknown,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const entry = Array.from(entityStore.entries()).find(([, record]) => record[field] === value)
|
||||
if (!entry) {
|
||||
throw DBALError.notFound(`${entity} not found`)
|
||||
}
|
||||
const [id, record] = entry
|
||||
const next = { ...record, ...data }
|
||||
entityStore.set(id, next)
|
||||
return next
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const entry = Array.from(entityStore.entries()).find(([, record]) => record[field] === value)
|
||||
if (!entry) {
|
||||
return false
|
||||
}
|
||||
return entityStore.delete(entry[0])
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const candidates = Array.from(entityStore.entries()).filter(([, record]) =>
|
||||
Object.entries(filter ?? {}).every(([key, value]) => record[key] === value),
|
||||
)
|
||||
let deleted = 0
|
||||
for (const [id] of candidates) {
|
||||
if (entityStore.delete(id)) {
|
||||
deleted += 1
|
||||
}
|
||||
}
|
||||
return deleted
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
if (!data || data.length === 0) return 0
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const records = data.map((item) => ({ id: getRecordId(entity, item), record: { ...item } }))
|
||||
for (const { id } of records) {
|
||||
if (entityStore.has(id)) {
|
||||
throw DBALError.conflict(`${entity} already exists: ${id}`)
|
||||
}
|
||||
}
|
||||
records.forEach(({ id, record }) => {
|
||||
entityStore.set(id, record)
|
||||
})
|
||||
return records.length
|
||||
}
|
||||
|
||||
async updateMany(
|
||||
entity: string,
|
||||
filter: Record<string, unknown>,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<number> {
|
||||
const entityStore = this.getEntityStore(entity)
|
||||
const entries = Array.from(entityStore.entries())
|
||||
const matches = entries.filter(([, record]) =>
|
||||
Object.entries(filter).every(([key, value]) => record[key] === value),
|
||||
)
|
||||
matches.forEach(([id, record]) => {
|
||||
entityStore.set(id, { ...record, ...data })
|
||||
})
|
||||
return matches.length
|
||||
}
|
||||
|
||||
getCapabilities(): Promise<AdapterCapabilities> {
|
||||
return Promise.resolve({
|
||||
transactions: false,
|
||||
joins: false,
|
||||
fullTextSearch: false,
|
||||
ttl: false,
|
||||
jsonQueries: false,
|
||||
aggregations: false,
|
||||
relations: false,
|
||||
})
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.store.clear()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { DBALConfig } from '../../runtime/config'
|
||||
import type { DBALAdapter } from '../../adapters/adapter'
|
||||
import { DBALError } from '../foundation/errors'
|
||||
import { PrismaAdapter, PostgresAdapter, MySQLAdapter } from '../../adapters/prisma'
|
||||
import { MemoryAdapter } from '../../adapters/memory'
|
||||
import { ACLAdapter } from '../../adapters/acl-adapter'
|
||||
import { WebSocketBridge } from '../../bridges/websocket-bridge'
|
||||
|
||||
@@ -28,6 +29,9 @@ export const createAdapter = (config: DBALConfig): DBALAdapter => {
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'memory':
|
||||
baseAdapter = new MemoryAdapter()
|
||||
break
|
||||
case 'postgres':
|
||||
baseAdapter = new PostgresAdapter(
|
||||
config.database?.url,
|
||||
|
||||
@@ -6,7 +6,7 @@ export const validateClientConfig = (config: DBALConfig): DBALConfig => {
|
||||
throw DBALError.validationError('Adapter type must be specified', [])
|
||||
}
|
||||
|
||||
if (config.mode !== 'production' && !config.database?.url) {
|
||||
if (config.mode !== 'production' && config.adapter !== 'memory' && !config.database?.url) {
|
||||
throw DBALError.validationError('Database URL must be specified for non-production mode', [])
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export const extendSession = async (
|
||||
return { success: false, error: { code: 'VALIDATION_ERROR', message: idErrors[0] || 'Invalid ID' } }
|
||||
}
|
||||
|
||||
if (additionalSeconds <= 0) {
|
||||
return { success: false, error: { code: 'VALIDATION_ERROR', message: 'Additional seconds must be positive' } }
|
||||
if (additionalSeconds <= 0 || !Number.isInteger(additionalSeconds)) {
|
||||
return { success: false, error: { code: 'VALIDATION_ERROR', message: 'Additional seconds must be a positive integer' } }
|
||||
}
|
||||
|
||||
const session = store.sessions.get(id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface DBALConfig {
|
||||
mode: 'development' | 'production'
|
||||
adapter: 'prisma' | 'sqlite' | 'mongodb' | 'postgres' | 'mysql'
|
||||
adapter: 'prisma' | 'sqlite' | 'mongodb' | 'postgres' | 'mysql' | 'memory'
|
||||
tenantId?: string
|
||||
endpoint?: string
|
||||
auth?: {
|
||||
@@ -24,7 +24,7 @@ export interface DBALConfig {
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
role: 'public' | 'user' | 'moderator' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
|
||||
@@ -9,7 +9,8 @@ describe('lua script in-memory operations', () => {
|
||||
|
||||
const userResult = await createUser(store, {
|
||||
username: 'lua_owner',
|
||||
email: 'lua_owner@example.com'
|
||||
email: 'lua_owner@example.com',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
expect(userResult.success).toBe(true)
|
||||
@@ -55,7 +56,8 @@ describe('lua script in-memory operations', () => {
|
||||
|
||||
const userResult = await createUser(store, {
|
||||
username: 'lua_owner',
|
||||
email: 'lua_owner@example.com'
|
||||
email: 'lua_owner@example.com',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
expect(userResult.success).toBe(true)
|
||||
|
||||
@@ -8,7 +8,8 @@ describe('session in-memory operations', () => {
|
||||
const store = createInMemoryStore()
|
||||
const userResult = await createUser(store, {
|
||||
username: 'session_owner',
|
||||
email: 'session_owner@example.com'
|
||||
email: 'session_owner@example.com',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
expect(userResult.success).toBe(true)
|
||||
@@ -58,7 +59,8 @@ describe('session in-memory operations', () => {
|
||||
const store = createInMemoryStore()
|
||||
const userResult = await createUser(store, {
|
||||
username: 'session_owner',
|
||||
email: 'session_owner@example.com'
|
||||
email: 'session_owner@example.com',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
expect(userResult.success).toBe(true)
|
||||
|
||||
@@ -164,38 +164,45 @@ struct MoveComponentInput {
|
||||
int order = 0;
|
||||
};
|
||||
|
||||
struct Workflow {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::optional<std::string> description;
|
||||
std::string trigger;
|
||||
Json trigger_config;
|
||||
Json steps;
|
||||
bool is_active;
|
||||
std::string created_by;
|
||||
Timestamp created_at;
|
||||
Timestamp updated_at;
|
||||
};
|
||||
|
||||
struct CreateWorkflowInput {
|
||||
std::string name;
|
||||
std::optional<std::string> description;
|
||||
std::string trigger;
|
||||
Json trigger_config;
|
||||
Json steps;
|
||||
bool is_active = true;
|
||||
std::string created_by;
|
||||
};
|
||||
|
||||
struct UpdateWorkflowInput {
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
std::optional<std::string> trigger;
|
||||
std::optional<Json> trigger_config;
|
||||
std::optional<Json> steps;
|
||||
std::optional<bool> is_active;
|
||||
std::optional<std::string> created_by;
|
||||
};
|
||||
struct Workflow {
|
||||
std::string id;
|
||||
std::optional<std::string> tenant_id;
|
||||
std::string name;
|
||||
std::optional<std::string> description;
|
||||
std::string nodes;
|
||||
std::string edges;
|
||||
bool enabled;
|
||||
int version = 1;
|
||||
std::optional<Timestamp> created_at;
|
||||
std::optional<Timestamp> updated_at;
|
||||
std::optional<std::string> created_by;
|
||||
};
|
||||
|
||||
struct CreateWorkflowInput {
|
||||
std::optional<std::string> tenant_id;
|
||||
std::string name;
|
||||
std::optional<std::string> description;
|
||||
std::string nodes;
|
||||
std::string edges;
|
||||
bool enabled;
|
||||
int version = 1;
|
||||
std::optional<Timestamp> created_at;
|
||||
std::optional<Timestamp> updated_at;
|
||||
std::optional<std::string> created_by;
|
||||
};
|
||||
|
||||
struct UpdateWorkflowInput {
|
||||
std::optional<std::string> tenant_id;
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
std::optional<std::string> nodes;
|
||||
std::optional<std::string> edges;
|
||||
std::optional<bool> enabled;
|
||||
std::optional<int> version;
|
||||
std::optional<Timestamp> created_at;
|
||||
std::optional<Timestamp> updated_at;
|
||||
std::optional<std::string> created_by;
|
||||
};
|
||||
|
||||
struct Session {
|
||||
std::string id;
|
||||
|
||||
@@ -319,17 +319,17 @@ public:
|
||||
return Error::notImplemented("SQL adapter listLuaScripts");
|
||||
}
|
||||
|
||||
Result<Package> createPackage(const CreatePackageInput& input) override {
|
||||
Result<InstalledPackage> createPackage(const CreatePackageInput& input) override {
|
||||
(void)input;
|
||||
return Error::notImplemented("SQL adapter createPackage");
|
||||
}
|
||||
|
||||
Result<Package> getPackage(const std::string& id) override {
|
||||
Result<InstalledPackage> getPackage(const std::string& id) override {
|
||||
(void)id;
|
||||
return Error::notImplemented("SQL adapter getPackage");
|
||||
}
|
||||
|
||||
Result<Package> updatePackage(const std::string& id, const UpdatePackageInput& input) override {
|
||||
Result<InstalledPackage> updatePackage(const std::string& id, const UpdatePackageInput& input) override {
|
||||
(void)id;
|
||||
(void)input;
|
||||
return Error::notImplemented("SQL adapter updatePackage");
|
||||
@@ -340,7 +340,7 @@ public:
|
||||
return Error::notImplemented("SQL adapter deletePackage");
|
||||
}
|
||||
|
||||
Result<std::vector<Package>> listPackages(const ListOptions& options) override {
|
||||
Result<std::vector<InstalledPackage>> listPackages(const ListOptions& options) override {
|
||||
(void)options;
|
||||
return Error::notImplemented("SQL adapter listPackages");
|
||||
}
|
||||
|
||||
@@ -90,15 +90,16 @@ public:
|
||||
Result<Workflow> createWorkflow(const CreateWorkflowInput& input) override {
|
||||
Workflow workflow;
|
||||
workflow.id = "workflow_" + input.name;
|
||||
workflow.tenant_id = input.tenant_id;
|
||||
workflow.name = input.name;
|
||||
workflow.description = input.description;
|
||||
workflow.trigger = input.trigger;
|
||||
workflow.trigger_config = input.trigger_config;
|
||||
workflow.steps = input.steps;
|
||||
workflow.is_active = input.is_active;
|
||||
workflow.nodes = input.nodes;
|
||||
workflow.edges = input.edges;
|
||||
workflow.enabled = input.enabled;
|
||||
workflow.version = input.version;
|
||||
workflow.created_by = input.created_by;
|
||||
workflow.created_at = std::chrono::system_clock::now();
|
||||
workflow.updated_at = workflow.created_at;
|
||||
workflow.created_at = input.created_at.value_or(std::chrono::system_clock::now());
|
||||
workflow.updated_at = input.updated_at.value_or(workflow.created_at);
|
||||
|
||||
return Result<Workflow>(workflow);
|
||||
}
|
||||
|
||||
@@ -225,15 +225,15 @@ Result<std::vector<LuaScript>> Client::searchLuaScripts(const std::string& query
|
||||
return entities::lua_script::search(getStore(), query, created_by, limit);
|
||||
}
|
||||
|
||||
Result<Package> Client::createPackage(const CreatePackageInput& input) {
|
||||
Result<InstalledPackage> Client::createPackage(const CreatePackageInput& input) {
|
||||
return entities::package::create(getStore(), input);
|
||||
}
|
||||
|
||||
Result<Package> Client::getPackage(const std::string& id) {
|
||||
Result<InstalledPackage> Client::getPackage(const std::string& id) {
|
||||
return entities::package::get(getStore(), id);
|
||||
}
|
||||
|
||||
Result<Package> Client::updatePackage(const std::string& id, const UpdatePackageInput& input) {
|
||||
Result<InstalledPackage> Client::updatePackage(const std::string& id, const UpdatePackageInput& input) {
|
||||
return entities::package::update(getStore(), id, input);
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ Result<bool> Client::deletePackage(const std::string& id) {
|
||||
return entities::package::remove(getStore(), id);
|
||||
}
|
||||
|
||||
Result<std::vector<Package>> Client::listPackages(const ListOptions& options) {
|
||||
Result<std::vector<InstalledPackage>> Client::listPackages(const ListOptions& options) {
|
||||
return entities::package::list(getStore(), options);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ inline Result<int> batchCreatePackages(InMemoryStore& store, const std::vector<C
|
||||
for (const auto& id : created_ids) {
|
||||
auto it = store.packages.find(id);
|
||||
if (it != store.packages.end()) {
|
||||
store.package_keys.erase(validation::packageKey(it->second.name, it->second.version));
|
||||
store.package_keys.erase(validation::packageKey(it->second.package_id));
|
||||
store.packages.erase(it);
|
||||
}
|
||||
}
|
||||
return result.error();
|
||||
}
|
||||
created_ids.push_back(result.value().id);
|
||||
created_ids.push_back(result.value().package_id);
|
||||
}
|
||||
|
||||
return Result<int>(static_cast<int>(created_ids.size()));
|
||||
|
||||
@@ -32,13 +32,13 @@ inline Result<int> batchCreate(InMemoryStore& store, const std::vector<CreatePac
|
||||
for (const auto& id : created_ids) {
|
||||
auto it = store.packages.find(id);
|
||||
if (it != store.packages.end()) {
|
||||
store.package_keys.erase(validation::packageKey(it->second.name, it->second.version));
|
||||
store.package_keys.erase(validation::packageKey(it->second.package_id));
|
||||
store.packages.erase(it);
|
||||
}
|
||||
}
|
||||
return result.error();
|
||||
}
|
||||
created_ids.push_back(result.value().id);
|
||||
created_ids.push_back(result.value().package_id);
|
||||
}
|
||||
|
||||
return Result<int>(static_cast<int>(created_ids.size()));
|
||||
|
||||
@@ -27,7 +27,7 @@ inline Result<bool> remove(InMemoryStore& store, const std::string& id) {
|
||||
return Error::notFound("Package not found: " + id);
|
||||
}
|
||||
|
||||
store.package_keys.erase(validation::packageKey(it->second.name, it->second.version));
|
||||
store.package_keys.erase(validation::packageKey(it->second.package_id));
|
||||
store.packages.erase(it);
|
||||
|
||||
return Result<bool>(true);
|
||||
|
||||
@@ -17,53 +17,55 @@ namespace package {
|
||||
/**
|
||||
* List packages with filtering and pagination
|
||||
*/
|
||||
inline Result<std::vector<Package>> list(InMemoryStore& store, const ListOptions& options) {
|
||||
std::vector<Package> packages;
|
||||
inline Result<std::vector<InstalledPackage>> list(InMemoryStore& store, const ListOptions& options) {
|
||||
std::vector<InstalledPackage> packages;
|
||||
|
||||
for (const auto& [id, package] : store.packages) {
|
||||
bool matches = true;
|
||||
|
||||
if (options.filter.find("name") != options.filter.end()) {
|
||||
if (package.name != options.filter.at("name")) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("version") != options.filter.end()) {
|
||||
if (package.version != options.filter.at("version")) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("author") != options.filter.end()) {
|
||||
if (package.author != options.filter.at("author")) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("is_installed") != options.filter.end()) {
|
||||
bool filter_installed = options.filter.at("is_installed") == "true";
|
||||
if (package.is_installed != filter_installed) matches = false;
|
||||
}
|
||||
if (options.filter.find("package_id") != options.filter.end()) {
|
||||
if (package.package_id != options.filter.at("package_id")) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("version") != options.filter.end()) {
|
||||
if (package.version != options.filter.at("version")) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("tenant_id") != options.filter.end()) {
|
||||
if (!package.tenant_id.has_value() || package.tenant_id.value() != options.filter.at("tenant_id")) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.filter.find("enabled") != options.filter.end()) {
|
||||
bool filter_enabled = options.filter.at("enabled") == "true";
|
||||
if (package.enabled != filter_enabled) matches = false;
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
packages.push_back(package);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.sort.find("name") != options.sort.end()) {
|
||||
std::sort(packages.begin(), packages.end(), [](const Package& a, const Package& b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
} else if (options.sort.find("created_at") != options.sort.end()) {
|
||||
std::sort(packages.begin(), packages.end(), [](const Package& a, const Package& b) {
|
||||
return a.created_at < b.created_at;
|
||||
});
|
||||
}
|
||||
if (options.sort.find("package_id") != options.sort.end()) {
|
||||
std::sort(packages.begin(), packages.end(), [](const InstalledPackage& a, const InstalledPackage& b) {
|
||||
return a.package_id < b.package_id;
|
||||
});
|
||||
} else if (options.sort.find("created_at") != options.sort.end()) {
|
||||
std::sort(packages.begin(), packages.end(), [](const InstalledPackage& a, const InstalledPackage& b) {
|
||||
return a.created_at < b.created_at;
|
||||
});
|
||||
}
|
||||
|
||||
int start = (options.page - 1) * options.limit;
|
||||
int end = std::min(start + options.limit, static_cast<int>(packages.size()));
|
||||
|
||||
if (start < static_cast<int>(packages.size())) {
|
||||
return Result<std::vector<Package>>(std::vector<Package>(packages.begin() + start, packages.begin() + end));
|
||||
}
|
||||
|
||||
return Result<std::vector<Package>>(std::vector<Package>());
|
||||
}
|
||||
if (start < static_cast<int>(packages.size())) {
|
||||
return Result<std::vector<InstalledPackage>>(std::vector<InstalledPackage>(packages.begin() + start, packages.begin() + end));
|
||||
}
|
||||
|
||||
return Result<std::vector<InstalledPackage>>(std::vector<InstalledPackage>());
|
||||
}
|
||||
|
||||
} // namespace package
|
||||
} // namespace entities
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace package {
|
||||
/**
|
||||
* Update an existing package
|
||||
*/
|
||||
inline Result<Package> update(InMemoryStore& store, const std::string& id, const UpdatePackageInput& input) {
|
||||
inline Result<InstalledPackage> update(InMemoryStore& store, const std::string& id, const UpdatePackageInput& input) {
|
||||
if (id.empty()) {
|
||||
return Error::validationError("Package ID cannot be empty");
|
||||
}
|
||||
@@ -27,67 +27,35 @@ inline Result<Package> update(InMemoryStore& store, const std::string& id, const
|
||||
return Error::notFound("Package not found: " + id);
|
||||
}
|
||||
|
||||
Package& package = it->second;
|
||||
|
||||
std::string next_name = input.name.value_or(package.name);
|
||||
std::string next_version = input.version.value_or(package.version);
|
||||
|
||||
if (!validation::isValidPackageName(next_name)) {
|
||||
return Error::validationError("Package name must be 1-255 characters");
|
||||
}
|
||||
if (!validation::isValidSemver(next_version)) {
|
||||
return Error::validationError("Version must be valid semver");
|
||||
}
|
||||
|
||||
std::string current_key = validation::packageKey(package.name, package.version);
|
||||
std::string next_key = validation::packageKey(next_name, next_version);
|
||||
|
||||
if (next_key != current_key) {
|
||||
auto key_it = store.package_keys.find(next_key);
|
||||
if (key_it != store.package_keys.end() && key_it->second != id) {
|
||||
return Error::conflict("Package name+version already exists: " + next_key);
|
||||
}
|
||||
store.package_keys.erase(current_key);
|
||||
store.package_keys[next_key] = id;
|
||||
}
|
||||
|
||||
package.name = next_name;
|
||||
package.version = next_version;
|
||||
|
||||
if (input.description.has_value()) {
|
||||
package.description = input.description.value();
|
||||
}
|
||||
|
||||
if (input.author.has_value()) {
|
||||
if (input.author.value().empty()) {
|
||||
return Error::validationError("author is required");
|
||||
}
|
||||
package.author = input.author.value();
|
||||
}
|
||||
|
||||
if (input.manifest.has_value()) {
|
||||
package.manifest = input.manifest.value();
|
||||
}
|
||||
|
||||
if (input.is_installed.has_value()) {
|
||||
package.is_installed = input.is_installed.value();
|
||||
}
|
||||
|
||||
if (input.installed_at.has_value()) {
|
||||
package.installed_at = input.installed_at.value();
|
||||
}
|
||||
|
||||
if (input.installed_by.has_value()) {
|
||||
if (input.installed_by.value().empty()) {
|
||||
return Error::validationError("installed_by is required");
|
||||
}
|
||||
package.installed_by = input.installed_by.value();
|
||||
}
|
||||
InstalledPackage& package = it->second;
|
||||
|
||||
std::string next_version = input.version.value_or(package.version);
|
||||
if (!validation::isValidSemver(next_version)) {
|
||||
return Error::validationError("Version must be valid semver");
|
||||
}
|
||||
|
||||
package.version = next_version;
|
||||
|
||||
if (input.tenant_id.has_value()) {
|
||||
package.tenant_id = input.tenant_id.value();
|
||||
}
|
||||
|
||||
if (input.installed_at.has_value()) {
|
||||
package.installed_at = input.installed_at.value();
|
||||
}
|
||||
|
||||
if (input.enabled.has_value()) {
|
||||
package.enabled = input.enabled.value();
|
||||
}
|
||||
|
||||
if (input.config.has_value()) {
|
||||
package.config = input.config.value();
|
||||
}
|
||||
|
||||
package.updated_at = std::chrono::system_clock::now();
|
||||
|
||||
return Result<Package>(package);
|
||||
}
|
||||
return Result<InstalledPackage>(package);
|
||||
}
|
||||
|
||||
} // namespace package
|
||||
} // namespace entities
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file get_page.hpp
|
||||
* @brief Get page by ID or slug operations
|
||||
* @brief Get page by ID operations
|
||||
*/
|
||||
#ifndef DBAL_GET_PAGE_HPP
|
||||
#define DBAL_GET_PAGE_HPP
|
||||
|
||||
@@ -21,28 +21,22 @@ inline Result<Workflow> create(InMemoryStore& store, const CreateWorkflowInput&
|
||||
if (!validation::isValidWorkflowName(input.name)) {
|
||||
return Error::validationError("Workflow name must be 1-255 characters");
|
||||
}
|
||||
if (!validation::isValidWorkflowTrigger(input.trigger)) {
|
||||
return Error::validationError("Trigger must be one of manual, schedule, event, webhook");
|
||||
}
|
||||
if (input.created_by.empty()) {
|
||||
return Error::validationError("created_by is required");
|
||||
}
|
||||
|
||||
if (store.workflow_names.find(input.name) != store.workflow_names.end()) {
|
||||
return Error::conflict("Workflow name already exists: " + input.name);
|
||||
}
|
||||
|
||||
Workflow workflow;
|
||||
workflow.id = store.generateId("workflow", ++store.workflow_counter);
|
||||
workflow.name = input.name;
|
||||
workflow.description = input.description;
|
||||
workflow.trigger = input.trigger;
|
||||
workflow.trigger_config = input.trigger_config;
|
||||
workflow.steps = input.steps;
|
||||
workflow.is_active = input.is_active;
|
||||
workflow.created_by = input.created_by;
|
||||
workflow.created_at = std::chrono::system_clock::now();
|
||||
workflow.updated_at = workflow.created_at;
|
||||
if (store.workflow_names.find(input.name) != store.workflow_names.end()) {
|
||||
return Error::conflict("Workflow name already exists: " + input.name);
|
||||
}
|
||||
|
||||
Workflow workflow;
|
||||
workflow.id = store.generateId("workflow", ++store.workflow_counter);
|
||||
workflow.tenant_id = input.tenant_id;
|
||||
workflow.name = input.name;
|
||||
workflow.description = input.description;
|
||||
workflow.nodes = input.nodes;
|
||||
workflow.edges = input.edges;
|
||||
workflow.enabled = input.enabled;
|
||||
workflow.version = input.version;
|
||||
workflow.created_at = input.created_at.value_or(std::chrono::system_clock::now());
|
||||
workflow.updated_at = input.updated_at.value_or(workflow.created_at);
|
||||
workflow.created_by = input.created_by;
|
||||
|
||||
store.workflows[workflow.id] = workflow;
|
||||
store.workflow_names[workflow.name] = workflow.id;
|
||||
|
||||
@@ -23,18 +23,22 @@ inline Result<std::vector<Workflow>> list(InMemoryStore& store, const ListOption
|
||||
for (const auto& [id, workflow] : store.workflows) {
|
||||
bool matches = true;
|
||||
|
||||
if (options.filter.find("is_active") != options.filter.end()) {
|
||||
bool filter_active = options.filter.at("is_active") == "true";
|
||||
if (workflow.is_active != filter_active) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("trigger") != options.filter.end()) {
|
||||
if (workflow.trigger != options.filter.at("trigger")) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("created_by") != options.filter.end()) {
|
||||
if (workflow.created_by != options.filter.at("created_by")) matches = false;
|
||||
}
|
||||
if (options.filter.find("enabled") != options.filter.end()) {
|
||||
bool filter_enabled = options.filter.at("enabled") == "true";
|
||||
if (workflow.enabled != filter_enabled) matches = false;
|
||||
}
|
||||
|
||||
if (options.filter.find("tenant_id") != options.filter.end()) {
|
||||
if (!workflow.tenant_id.has_value() || workflow.tenant_id.value() != options.filter.at("tenant_id")) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.filter.find("created_by") != options.filter.end()) {
|
||||
if (!workflow.created_by.has_value() || workflow.created_by.value() != options.filter.at("created_by")) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
workflows.push_back(workflow);
|
||||
@@ -45,11 +49,12 @@ inline Result<std::vector<Workflow>> list(InMemoryStore& store, const ListOption
|
||||
std::sort(workflows.begin(), workflows.end(), [](const Workflow& a, const Workflow& b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
} else if (options.sort.find("created_at") != options.sort.end()) {
|
||||
std::sort(workflows.begin(), workflows.end(), [](const Workflow& a, const Workflow& b) {
|
||||
return a.created_at < b.created_at;
|
||||
});
|
||||
}
|
||||
} else if (options.sort.find("created_at") != options.sort.end()) {
|
||||
std::sort(workflows.begin(), workflows.end(), [](const Workflow& a, const Workflow& b) {
|
||||
return a.created_at.value_or(std::chrono::system_clock::time_point()) <
|
||||
b.created_at.value_or(std::chrono::system_clock::time_point());
|
||||
});
|
||||
}
|
||||
|
||||
int start = (options.page - 1) * options.limit;
|
||||
int end = std::min(start + options.limit, static_cast<int>(workflows.size()));
|
||||
|
||||
@@ -43,37 +43,43 @@ inline Result<Workflow> update(InMemoryStore& store, const std::string& id, cons
|
||||
workflow.name = input.name.value();
|
||||
}
|
||||
|
||||
if (input.description.has_value()) {
|
||||
workflow.description = input.description.value();
|
||||
}
|
||||
|
||||
if (input.trigger.has_value()) {
|
||||
if (!validation::isValidWorkflowTrigger(input.trigger.value())) {
|
||||
return Error::validationError("Trigger must be one of manual, schedule, event, webhook");
|
||||
}
|
||||
workflow.trigger = input.trigger.value();
|
||||
}
|
||||
|
||||
if (input.trigger_config.has_value()) {
|
||||
workflow.trigger_config = input.trigger_config.value();
|
||||
}
|
||||
|
||||
if (input.steps.has_value()) {
|
||||
workflow.steps = input.steps.value();
|
||||
}
|
||||
|
||||
if (input.is_active.has_value()) {
|
||||
workflow.is_active = input.is_active.value();
|
||||
}
|
||||
|
||||
if (input.created_by.has_value()) {
|
||||
if (input.created_by.value().empty()) {
|
||||
return Error::validationError("created_by is required");
|
||||
}
|
||||
workflow.created_by = input.created_by.value();
|
||||
}
|
||||
|
||||
workflow.updated_at = std::chrono::system_clock::now();
|
||||
if (input.description.has_value()) {
|
||||
workflow.description = input.description.value();
|
||||
}
|
||||
|
||||
if (input.nodes.has_value()) {
|
||||
workflow.nodes = input.nodes.value();
|
||||
}
|
||||
|
||||
if (input.edges.has_value()) {
|
||||
workflow.edges = input.edges.value();
|
||||
}
|
||||
|
||||
if (input.enabled.has_value()) {
|
||||
workflow.enabled = input.enabled.value();
|
||||
}
|
||||
|
||||
if (input.version.has_value()) {
|
||||
workflow.version = input.version.value();
|
||||
}
|
||||
|
||||
if (input.created_by.has_value()) {
|
||||
workflow.created_by = input.created_by.value();
|
||||
}
|
||||
|
||||
if (input.created_at.has_value()) {
|
||||
workflow.created_at = input.created_at.value();
|
||||
}
|
||||
|
||||
if (input.updated_at.has_value()) {
|
||||
workflow.updated_at = input.updated_at.value();
|
||||
} else {
|
||||
workflow.updated_at = std::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
if (input.tenant_id.has_value()) {
|
||||
workflow.tenant_id = input.tenant_id.value();
|
||||
}
|
||||
|
||||
return Result<Workflow>(workflow);
|
||||
}
|
||||
|
||||
@@ -19,14 +19,6 @@ inline bool isValidWorkflowName(const std::string& name) {
|
||||
return !name.empty() && name.length() <= 255;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate workflow trigger type
|
||||
*/
|
||||
inline bool isValidWorkflowTrigger(const std::string& trigger) {
|
||||
static const std::array<std::string, 4> allowed = {"manual", "schedule", "event", "webhook"};
|
||||
return std::find(allowed.begin(), allowed.end(), trigger) != allowed.end();
|
||||
}
|
||||
|
||||
} // namespace validation
|
||||
} // namespace dbal
|
||||
|
||||
|
||||
@@ -520,15 +520,17 @@ void test_page_crud() {
|
||||
|
||||
// Create page
|
||||
dbal::CreatePageInput input;
|
||||
input.slug = "test-page";
|
||||
input.path = "/test-page";
|
||||
input.title = "Test Page";
|
||||
input.description = "A test page";
|
||||
input.level = 2;
|
||||
input.is_active = true;
|
||||
input.component_tree = "{}";
|
||||
input.requires_auth = false;
|
||||
input.is_published = true;
|
||||
|
||||
auto createResult = client.createPage(input);
|
||||
assert(createResult.isOk());
|
||||
assert(createResult.value().slug == "test-page");
|
||||
assert(createResult.value().path == "/test-page");
|
||||
std::string pageId = createResult.value().id;
|
||||
std::cout << " ✓ Page created with ID: " << pageId << std::endl;
|
||||
|
||||
@@ -538,11 +540,11 @@ void test_page_crud() {
|
||||
assert(getResult.value().title == "Test Page");
|
||||
std::cout << " ✓ Retrieved page by ID" << std::endl;
|
||||
|
||||
// Get by slug
|
||||
auto getBySlugResult = client.getPageByPath("test-page");
|
||||
// Get by path
|
||||
auto getBySlugResult = client.getPageByPath("/test-page");
|
||||
assert(getBySlugResult.isOk());
|
||||
assert(getBySlugResult.value().id == pageId);
|
||||
std::cout << " ✓ Retrieved page by slug" << std::endl;
|
||||
std::cout << " ✓ Retrieved page by path" << std::endl;
|
||||
|
||||
// Update page
|
||||
dbal::UpdatePageInput updateInput;
|
||||
@@ -570,21 +572,25 @@ void test_page_validation() {
|
||||
config.database_url = ":memory:";
|
||||
dbal::Client client(config);
|
||||
|
||||
// Invalid slug (uppercase)
|
||||
// Invalid path (empty)
|
||||
dbal::CreatePageInput input1;
|
||||
input1.slug = "Invalid-Slug";
|
||||
input1.path = "";
|
||||
input1.title = "Test";
|
||||
input1.level = 1;
|
||||
input1.component_tree = "{}";
|
||||
input1.requires_auth = false;
|
||||
auto result1 = client.createPage(input1);
|
||||
assert(result1.isError());
|
||||
assert(result1.error().code() == dbal::ErrorCode::ValidationError);
|
||||
std::cout << " ✓ Uppercase slug rejected" << std::endl;
|
||||
std::cout << " ✓ Empty path rejected" << std::endl;
|
||||
|
||||
// Empty title
|
||||
dbal::CreatePageInput input2;
|
||||
input2.slug = "valid-slug";
|
||||
input2.path = "/valid-path";
|
||||
input2.title = "";
|
||||
input2.level = 1;
|
||||
input2.component_tree = "{}";
|
||||
input2.requires_auth = false;
|
||||
auto result2 = client.createPage(input2);
|
||||
assert(result2.isError());
|
||||
assert(result2.error().code() == dbal::ErrorCode::ValidationError);
|
||||
@@ -592,9 +598,11 @@ void test_page_validation() {
|
||||
|
||||
// Invalid level
|
||||
dbal::CreatePageInput input3;
|
||||
input3.slug = "valid-slug";
|
||||
input3.path = "/valid-path-2";
|
||||
input3.title = "Test";
|
||||
input3.level = 10;
|
||||
input3.component_tree = "{}";
|
||||
input3.requires_auth = false;
|
||||
auto result3 = client.createPage(input3);
|
||||
assert(result3.isError());
|
||||
assert(result3.error().code() == dbal::ErrorCode::ValidationError);
|
||||
@@ -610,20 +618,22 @@ void test_page_search() {
|
||||
dbal::Client client(config);
|
||||
|
||||
dbal::CreatePageInput page1;
|
||||
page1.slug = "search-page";
|
||||
page1.path = "/search-page";
|
||||
page1.title = "Search Page";
|
||||
page1.level = 1;
|
||||
page1.layout = {{"row", "one"}};
|
||||
page1.is_active = true;
|
||||
page1.component_tree = "{}";
|
||||
page1.requires_auth = false;
|
||||
page1.is_published = true;
|
||||
auto result1 = client.createPage(page1);
|
||||
assert(result1.isOk());
|
||||
|
||||
dbal::CreatePageInput page2;
|
||||
page2.slug = "other-page";
|
||||
page2.path = "/other-page";
|
||||
page2.title = "Other Search";
|
||||
page2.level = 1;
|
||||
page2.layout = {{"row", "two"}};
|
||||
page2.is_active = true;
|
||||
page2.component_tree = "{}";
|
||||
page2.requires_auth = false;
|
||||
page2.is_published = true;
|
||||
auto result2 = client.createPage(page2);
|
||||
assert(result2.isOk());
|
||||
|
||||
@@ -652,11 +662,12 @@ void test_component_crud() {
|
||||
dbal::Client client(config);
|
||||
|
||||
dbal::CreatePageInput pageInput;
|
||||
pageInput.slug = "component-page";
|
||||
pageInput.path = "/component-page";
|
||||
pageInput.title = "Component Page";
|
||||
pageInput.level = 1;
|
||||
pageInput.layout = {{"region", "root"}};
|
||||
pageInput.is_active = true;
|
||||
pageInput.component_tree = "{}";
|
||||
pageInput.requires_auth = false;
|
||||
pageInput.is_published = true;
|
||||
|
||||
auto pageResult = client.createPage(pageInput);
|
||||
assert(pageResult.isOk());
|
||||
@@ -664,9 +675,9 @@ void test_component_crud() {
|
||||
|
||||
dbal::CreateComponentNodeInput rootInput;
|
||||
rootInput.page_id = pageId;
|
||||
rootInput.component_type = "Container";
|
||||
rootInput.type = "Container";
|
||||
rootInput.child_ids = "[]";
|
||||
rootInput.order = 0;
|
||||
rootInput.props = {{"role", "root"}};
|
||||
|
||||
auto rootResult = client.createComponent(rootInput);
|
||||
assert(rootResult.isOk());
|
||||
@@ -676,9 +687,9 @@ void test_component_crud() {
|
||||
dbal::CreateComponentNodeInput childInput;
|
||||
childInput.page_id = pageId;
|
||||
childInput.parent_id = rootId;
|
||||
childInput.component_type = "Button";
|
||||
childInput.type = "Button";
|
||||
childInput.child_ids = "[]";
|
||||
childInput.order = 1;
|
||||
childInput.props = {{"label", "Click"}};
|
||||
|
||||
auto childResult = client.createComponent(childInput);
|
||||
assert(childResult.isOk());
|
||||
@@ -688,9 +699,9 @@ void test_component_crud() {
|
||||
dbal::CreateComponentNodeInput siblingInput;
|
||||
siblingInput.page_id = pageId;
|
||||
siblingInput.parent_id = rootId;
|
||||
siblingInput.component_type = "Text";
|
||||
siblingInput.type = "Text";
|
||||
siblingInput.child_ids = "[]";
|
||||
siblingInput.order = 3;
|
||||
siblingInput.props = {{"content", "sidebar"}};
|
||||
|
||||
auto siblingResult = client.createComponent(siblingInput);
|
||||
assert(siblingResult.isOk());
|
||||
@@ -719,12 +730,12 @@ void test_component_crud() {
|
||||
|
||||
dbal::ListOptions typeFilter;
|
||||
typeFilter.filter["pageId"] = pageId;
|
||||
typeFilter.filter["component_type"] = "Text";
|
||||
typeFilter.filter["type"] = "Text";
|
||||
auto typeList = client.listComponents(typeFilter);
|
||||
assert(typeList.isOk());
|
||||
assert(!typeList.value().empty());
|
||||
for (const auto& entry : typeList.value()) {
|
||||
assert(entry.component_type == "Text");
|
||||
assert(entry.type == "Text");
|
||||
}
|
||||
std::cout << " ✓ Component type filter works" << std::endl;
|
||||
|
||||
@@ -742,9 +753,9 @@ void test_component_crud() {
|
||||
|
||||
dbal::CreateComponentNodeInput otherRootInput;
|
||||
otherRootInput.page_id = pageId;
|
||||
otherRootInput.component_type = "Sidebar";
|
||||
otherRootInput.type = "Sidebar";
|
||||
otherRootInput.child_ids = "[]";
|
||||
otherRootInput.order = 0;
|
||||
otherRootInput.props = {{"region", "secondary"}};
|
||||
|
||||
auto otherRootResult = client.createComponent(otherRootInput);
|
||||
assert(otherRootResult.isOk());
|
||||
@@ -785,11 +796,12 @@ void test_component_validation() {
|
||||
dbal::Client client(config);
|
||||
|
||||
dbal::CreatePageInput pageInput;
|
||||
pageInput.slug = "component-validation";
|
||||
pageInput.path = "/component-validation";
|
||||
pageInput.title = "Component Validation";
|
||||
pageInput.level = 1;
|
||||
pageInput.layout = {{"mode", "validate"}};
|
||||
pageInput.is_active = true;
|
||||
pageInput.component_tree = "{}";
|
||||
pageInput.requires_auth = false;
|
||||
pageInput.is_published = true;
|
||||
|
||||
auto pageResult = client.createPage(pageInput);
|
||||
assert(pageResult.isOk());
|
||||
@@ -797,9 +809,9 @@ void test_component_validation() {
|
||||
|
||||
dbal::CreateComponentNodeInput missingPage;
|
||||
missingPage.page_id = "missing-page";
|
||||
missingPage.component_type = "Leaf";
|
||||
missingPage.type = "Leaf";
|
||||
missingPage.child_ids = "[]";
|
||||
missingPage.order = 0;
|
||||
missingPage.props = {{"key", "value"}};
|
||||
auto missingResult = client.createComponent(missingPage);
|
||||
assert(missingResult.isError());
|
||||
assert(missingResult.error().code() == dbal::ErrorCode::NotFound);
|
||||
@@ -807,9 +819,9 @@ void test_component_validation() {
|
||||
|
||||
dbal::CreateComponentNodeInput longType;
|
||||
longType.page_id = pageId;
|
||||
longType.component_type = std::string(101, 'x');
|
||||
longType.type = std::string(101, 'x');
|
||||
longType.child_ids = "[]";
|
||||
longType.order = 0;
|
||||
longType.props = {{"key", "value"}};
|
||||
auto longResult = client.createComponent(longType);
|
||||
assert(longResult.isError());
|
||||
assert(longResult.error().code() == dbal::ErrorCode::ValidationError);
|
||||
@@ -817,9 +829,9 @@ void test_component_validation() {
|
||||
|
||||
dbal::CreateComponentNodeInput badOrder;
|
||||
badOrder.page_id = pageId;
|
||||
badOrder.component_type = "Leaf";
|
||||
badOrder.type = "Leaf";
|
||||
badOrder.child_ids = "[]";
|
||||
badOrder.order = -1;
|
||||
badOrder.props = {{"key", "value"}};
|
||||
auto orderResult = client.createComponent(badOrder);
|
||||
assert(orderResult.isError());
|
||||
assert(orderResult.error().code() == dbal::ErrorCode::ValidationError);
|
||||
@@ -835,20 +847,21 @@ void test_component_search() {
|
||||
dbal::Client client(config);
|
||||
|
||||
dbal::CreatePageInput pageInput;
|
||||
pageInput.slug = "component-search";
|
||||
pageInput.path = "/component-search";
|
||||
pageInput.title = "Component Search";
|
||||
pageInput.level = 1;
|
||||
pageInput.layout = {{"row", "search"}};
|
||||
pageInput.is_active = true;
|
||||
pageInput.component_tree = "{}";
|
||||
pageInput.requires_auth = false;
|
||||
pageInput.is_published = true;
|
||||
auto pageResult = client.createPage(pageInput);
|
||||
assert(pageResult.isOk());
|
||||
std::string pageId = pageResult.value().id;
|
||||
|
||||
dbal::CreateComponentNodeInput targetInput;
|
||||
targetInput.page_id = pageId;
|
||||
targetInput.component_type = "SearchButton";
|
||||
targetInput.type = "SearchButton";
|
||||
targetInput.child_ids = "[\"find-me\"]";
|
||||
targetInput.order = 0;
|
||||
targetInput.props = {{"payload", "find-me"}};
|
||||
auto targetResult = client.createComponent(targetInput);
|
||||
assert(targetResult.isOk());
|
||||
std::string targetId = targetResult.value().id;
|
||||
@@ -880,20 +893,21 @@ void test_component_children() {
|
||||
dbal::Client client(config);
|
||||
|
||||
dbal::CreatePageInput pageInput;
|
||||
pageInput.slug = "component-children";
|
||||
pageInput.path = "/component-children";
|
||||
pageInput.title = "Component Children";
|
||||
pageInput.level = 1;
|
||||
pageInput.layout = {{"tree", "root"}};
|
||||
pageInput.is_active = true;
|
||||
pageInput.component_tree = "{}";
|
||||
pageInput.requires_auth = false;
|
||||
pageInput.is_published = true;
|
||||
auto pageResult = client.createPage(pageInput);
|
||||
assert(pageResult.isOk());
|
||||
std::string pageId = pageResult.value().id;
|
||||
|
||||
dbal::CreateComponentNodeInput rootInput;
|
||||
rootInput.page_id = pageId;
|
||||
rootInput.component_type = "Root";
|
||||
rootInput.type = "Root";
|
||||
rootInput.child_ids = "[]";
|
||||
rootInput.order = 0;
|
||||
rootInput.props = {{"depth", "0"}};
|
||||
auto rootResult = client.createComponent(rootInput);
|
||||
assert(rootResult.isOk());
|
||||
std::string rootId = rootResult.value().id;
|
||||
@@ -901,9 +915,9 @@ void test_component_children() {
|
||||
dbal::CreateComponentNodeInput childInput;
|
||||
childInput.page_id = pageId;
|
||||
childInput.parent_id = rootId;
|
||||
childInput.component_type = "Child";
|
||||
childInput.type = "Child";
|
||||
childInput.child_ids = "[]";
|
||||
childInput.order = 0;
|
||||
childInput.props = {{"depth", "1"}};
|
||||
auto childResult = client.createComponent(childInput);
|
||||
assert(childResult.isOk());
|
||||
std::string childId = childResult.value().id;
|
||||
@@ -911,9 +925,9 @@ void test_component_children() {
|
||||
dbal::CreateComponentNodeInput grandchildInput;
|
||||
grandchildInput.page_id = pageId;
|
||||
grandchildInput.parent_id = childId;
|
||||
grandchildInput.component_type = "Grandchild";
|
||||
grandchildInput.type = "Grandchild";
|
||||
grandchildInput.child_ids = "[]";
|
||||
grandchildInput.order = 0;
|
||||
grandchildInput.props = {{"depth", "2"}};
|
||||
auto grandchildResult = client.createComponent(grandchildInput);
|
||||
assert(grandchildResult.isOk());
|
||||
|
||||
@@ -936,7 +950,7 @@ void test_component_children() {
|
||||
auto childChildren = client.getComponentChildren(childId);
|
||||
assert(childChildren.isOk());
|
||||
assert(childChildren.value().size() == 1);
|
||||
assert(childChildren.value()[0].component_type == "Grandchild");
|
||||
assert(childChildren.value()[0].type == "Grandchild");
|
||||
std::cout << " ✓ Retrieved grandchildren for child" << std::endl;
|
||||
|
||||
auto missing = client.getComponentChildren("nonexistent");
|
||||
@@ -964,10 +978,9 @@ void test_workflow_crud() {
|
||||
dbal::CreateWorkflowInput input;
|
||||
input.name = "workflow-crud";
|
||||
input.description = "Test workflow";
|
||||
input.trigger = "schedule";
|
||||
input.trigger_config = {{"cron", "0 0 * * *"}};
|
||||
input.steps = {{"step1", "noop"}};
|
||||
input.is_active = true;
|
||||
input.nodes = "[]";
|
||||
input.edges = "[]";
|
||||
input.enabled = true;
|
||||
input.created_by = userResult.value().id;
|
||||
|
||||
auto createResult = client.createWorkflow(input);
|
||||
@@ -985,20 +998,20 @@ void test_workflow_crud() {
|
||||
// Update workflow
|
||||
dbal::UpdateWorkflowInput updateInput;
|
||||
updateInput.name = "workflow-crud-updated";
|
||||
updateInput.is_active = false;
|
||||
updateInput.enabled = false;
|
||||
auto updateResult = client.updateWorkflow(workflowId, updateInput);
|
||||
assert(updateResult.isOk());
|
||||
assert(updateResult.value().name == "workflow-crud-updated");
|
||||
assert(updateResult.value().is_active == false);
|
||||
assert(updateResult.value().enabled == false);
|
||||
std::cout << " ✓ Workflow updated" << std::endl;
|
||||
|
||||
// List workflows
|
||||
dbal::ListOptions listOptions;
|
||||
listOptions.filter["is_active"] = "false";
|
||||
listOptions.filter["enabled"] = "false";
|
||||
auto listResult = client.listWorkflows(listOptions);
|
||||
assert(listResult.isOk());
|
||||
assert(listResult.value().size() >= 1);
|
||||
std::cout << " ✓ Listed workflows (filtered by is_active=false)" << std::endl;
|
||||
std::cout << " ✓ Listed workflows (filtered by enabled=false)" << std::endl;
|
||||
|
||||
// Delete workflow
|
||||
auto deleteResult = client.deleteWorkflow(workflowId);
|
||||
@@ -1025,45 +1038,33 @@ void test_workflow_validation() {
|
||||
auto userResult = client.createUser(userInput);
|
||||
assert(userResult.isOk());
|
||||
|
||||
// Invalid trigger
|
||||
// Empty name
|
||||
dbal::CreateWorkflowInput input1;
|
||||
input1.name = "invalid-trigger";
|
||||
input1.trigger = "invalid";
|
||||
input1.trigger_config = {{"cron", "* * * * *"}};
|
||||
input1.steps = {{"step1", "noop"}};
|
||||
input1.name = "";
|
||||
input1.nodes = "[]";
|
||||
input1.edges = "[]";
|
||||
input1.enabled = true;
|
||||
input1.created_by = userResult.value().id;
|
||||
auto result1 = client.createWorkflow(input1);
|
||||
assert(result1.isError());
|
||||
assert(result1.error().code() == dbal::ErrorCode::ValidationError);
|
||||
std::cout << " ✓ Invalid trigger rejected" << std::endl;
|
||||
|
||||
// Empty name
|
||||
dbal::CreateWorkflowInput input2;
|
||||
input2.name = "";
|
||||
input2.trigger = "manual";
|
||||
input2.trigger_config = {{"mode", "test"}};
|
||||
input2.steps = {{"step1", "noop"}};
|
||||
input2.created_by = userResult.value().id;
|
||||
auto result2 = client.createWorkflow(input2);
|
||||
assert(result2.isError());
|
||||
assert(result2.error().code() == dbal::ErrorCode::ValidationError);
|
||||
std::cout << " ✓ Empty name rejected" << std::endl;
|
||||
|
||||
// Duplicate name
|
||||
dbal::CreateWorkflowInput input3;
|
||||
input3.name = "workflow-duplicate";
|
||||
input3.trigger = "manual";
|
||||
input3.trigger_config = {{"mode", "test"}};
|
||||
input3.steps = {{"step1", "noop"}};
|
||||
dbal::CreateWorkflowInput input2;
|
||||
input2.name = "workflow-duplicate";
|
||||
input2.nodes = "[]";
|
||||
input2.edges = "[]";
|
||||
input2.enabled = true;
|
||||
input2.created_by = userResult.value().id;
|
||||
auto result2 = client.createWorkflow(input2);
|
||||
assert(result2.isOk());
|
||||
|
||||
dbal::CreateWorkflowInput input3 = input2;
|
||||
input3.created_by = userResult.value().id;
|
||||
auto result3 = client.createWorkflow(input3);
|
||||
assert(result3.isOk());
|
||||
|
||||
dbal::CreateWorkflowInput input4 = input3;
|
||||
input4.created_by = userResult.value().id;
|
||||
auto result4 = client.createWorkflow(input4);
|
||||
assert(result4.isError());
|
||||
assert(result4.error().code() == dbal::ErrorCode::Conflict);
|
||||
assert(result3.isError());
|
||||
assert(result3.error().code() == dbal::ErrorCode::Conflict);
|
||||
std::cout << " ✓ Duplicate workflow name rejected" << std::endl;
|
||||
}
|
||||
|
||||
@@ -1333,38 +1334,36 @@ void test_package_crud() {
|
||||
assert(userResult.isOk());
|
||||
|
||||
dbal::CreatePackageInput input;
|
||||
input.name = "forum";
|
||||
input.package_id = "forum";
|
||||
input.version = "1.2.3";
|
||||
input.description = "Forum package";
|
||||
input.author = "MetaBuilder";
|
||||
input.manifest = {{"entry", "index.lua"}};
|
||||
input.is_installed = false;
|
||||
input.installed_at = std::chrono::system_clock::now();
|
||||
input.enabled = false;
|
||||
input.config = "{\"entry\":\"index.lua\"}";
|
||||
|
||||
auto createResult = client.createPackage(input);
|
||||
assert(createResult.isOk());
|
||||
std::string packageId = createResult.value().id;
|
||||
std::string packageId = createResult.value().package_id;
|
||||
std::cout << " ✓ Package created with ID: " << packageId << std::endl;
|
||||
|
||||
auto getResult = client.getPackage(packageId);
|
||||
assert(getResult.isOk());
|
||||
assert(getResult.value().name == "forum");
|
||||
assert(getResult.value().package_id == "forum");
|
||||
std::cout << " ✓ Retrieved package by ID" << std::endl;
|
||||
|
||||
dbal::UpdatePackageInput updateInput;
|
||||
updateInput.is_installed = true;
|
||||
updateInput.installed_by = userResult.value().id;
|
||||
updateInput.enabled = true;
|
||||
updateInput.installed_at = std::chrono::system_clock::now();
|
||||
auto updateResult = client.updatePackage(packageId, updateInput);
|
||||
assert(updateResult.isOk());
|
||||
assert(updateResult.value().is_installed == true);
|
||||
assert(updateResult.value().enabled == true);
|
||||
std::cout << " ✓ Package updated" << std::endl;
|
||||
|
||||
dbal::ListOptions listOptions;
|
||||
listOptions.filter["is_installed"] = "true";
|
||||
listOptions.filter["enabled"] = "true";
|
||||
auto listResult = client.listPackages(listOptions);
|
||||
assert(listResult.isOk());
|
||||
assert(listResult.value().size() >= 1);
|
||||
std::cout << " ✓ Listed packages (filtered by is_installed=true)" << std::endl;
|
||||
std::cout << " ✓ Listed packages (filtered by enabled=true)" << std::endl;
|
||||
|
||||
auto deleteResult = client.deletePackage(packageId);
|
||||
assert(deleteResult.isOk());
|
||||
@@ -1383,20 +1382,20 @@ void test_package_validation() {
|
||||
dbal::Client client(config);
|
||||
|
||||
dbal::CreatePackageInput input1;
|
||||
input1.name = "invalid-package";
|
||||
input1.package_id = "invalid-package";
|
||||
input1.version = "bad";
|
||||
input1.author = "MetaBuilder";
|
||||
input1.manifest = {{"entry", "index.lua"}};
|
||||
input1.installed_at = std::chrono::system_clock::now();
|
||||
input1.enabled = false;
|
||||
auto result1 = client.createPackage(input1);
|
||||
assert(result1.isError());
|
||||
assert(result1.error().code() == dbal::ErrorCode::ValidationError);
|
||||
std::cout << " ✓ Invalid semver rejected" << std::endl;
|
||||
|
||||
dbal::CreatePackageInput input2;
|
||||
input2.name = "duplicate-package";
|
||||
input2.package_id = "duplicate-package";
|
||||
input2.version = "1.0.0";
|
||||
input2.author = "MetaBuilder";
|
||||
input2.manifest = {{"entry", "index.lua"}};
|
||||
input2.installed_at = std::chrono::system_clock::now();
|
||||
input2.enabled = false;
|
||||
auto result2 = client.createPackage(input2);
|
||||
assert(result2.isOk());
|
||||
|
||||
@@ -1404,7 +1403,7 @@ void test_package_validation() {
|
||||
auto result3 = client.createPackage(input3);
|
||||
assert(result3.isError());
|
||||
assert(result3.error().code() == dbal::ErrorCode::Conflict);
|
||||
std::cout << " ✓ Duplicate package version rejected" << std::endl;
|
||||
std::cout << " ✓ Duplicate package ID rejected" << std::endl;
|
||||
}
|
||||
|
||||
void test_package_batch_operations() {
|
||||
@@ -1417,17 +1416,17 @@ void test_package_batch_operations() {
|
||||
|
||||
std::vector<dbal::CreatePackageInput> packages;
|
||||
dbal::CreatePackageInput package1;
|
||||
package1.name = "batch-package-1";
|
||||
package1.package_id = "batch-package-1";
|
||||
package1.version = "1.0.0";
|
||||
package1.author = "MetaBuilder";
|
||||
package1.manifest = {{"entry", "index.lua"}};
|
||||
package1.installed_at = std::chrono::system_clock::now();
|
||||
package1.enabled = false;
|
||||
packages.push_back(package1);
|
||||
|
||||
dbal::CreatePackageInput package2;
|
||||
package2.name = "batch-package-2";
|
||||
package2.package_id = "batch-package-2";
|
||||
package2.version = "2.0.0";
|
||||
package2.author = "MetaBuilder";
|
||||
package2.manifest = {{"entry", "chat.lua"}};
|
||||
package2.installed_at = std::chrono::system_clock::now();
|
||||
package2.enabled = false;
|
||||
packages.push_back(package2);
|
||||
|
||||
auto createResult = client.batchCreatePackages(packages);
|
||||
@@ -1443,13 +1442,13 @@ void test_package_batch_operations() {
|
||||
|
||||
std::vector<dbal::UpdatePackageBatchItem> updates;
|
||||
dbal::UpdatePackageBatchItem update1;
|
||||
update1.id = listResult.value()[0].id;
|
||||
update1.data.is_installed = true;
|
||||
update1.id = listResult.value()[0].package_id;
|
||||
update1.data.enabled = true;
|
||||
updates.push_back(update1);
|
||||
|
||||
dbal::UpdatePackageBatchItem update2;
|
||||
update2.id = listResult.value()[1].id;
|
||||
update2.data.is_installed = true;
|
||||
update2.id = listResult.value()[1].package_id;
|
||||
update2.data.enabled = true;
|
||||
updates.push_back(update2);
|
||||
|
||||
auto updateResult = client.batchUpdatePackages(updates);
|
||||
@@ -1458,8 +1457,8 @@ void test_package_batch_operations() {
|
||||
std::cout << " ✓ Batch updated packages" << std::endl;
|
||||
|
||||
std::vector<std::string> ids;
|
||||
ids.push_back(listResult.value()[0].id);
|
||||
ids.push_back(listResult.value()[1].id);
|
||||
ids.push_back(listResult.value()[0].package_id);
|
||||
ids.push_back(listResult.value()[1].package_id);
|
||||
|
||||
auto deleteResult = client.batchDeletePackages(ids);
|
||||
assert(deleteResult.isOk());
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
// NOTE: This schema is a simplified dev snapshot for DBAL.
|
||||
// It is currently out of sync with the app's Prisma schema in `prisma/schema.prisma`.
|
||||
// TODO: Generate Prisma schemas from DBAL API schemas to keep them aligned.
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
email String @unique
|
||||
role String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
workflows Workflow[]
|
||||
luaScripts LuaScript[]
|
||||
installedPackages InstalledPackage[]
|
||||
}
|
||||
|
||||
model Credential {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
passwordHash String
|
||||
firstLogin Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
token String @unique
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
lastActivity DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
username String @unique
|
||||
email String @unique
|
||||
role String
|
||||
profilePicture String?
|
||||
bio String?
|
||||
createdAt BigInt
|
||||
tenantId String?
|
||||
isInstanceOwner Boolean @default(false)
|
||||
passwordChangeTimestamp BigInt?
|
||||
firstLogin Boolean @default(false)
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
model Credential {
|
||||
username String @id
|
||||
passwordHash String
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
userId String
|
||||
token String @unique
|
||||
expiresAt BigInt
|
||||
createdAt BigInt
|
||||
lastActivity BigInt
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
|
||||
@@index([userId])
|
||||
@@index([expiresAt])
|
||||
@@index([token])
|
||||
}
|
||||
|
||||
model PageConfig {
|
||||
id String @id @default(uuid())
|
||||
id String @id
|
||||
tenantId String?
|
||||
packageId String?
|
||||
path String @unique
|
||||
path String // Route pattern: /media/jobs, /forum/:id
|
||||
title String
|
||||
description String?
|
||||
icon String?
|
||||
component String?
|
||||
componentTree String
|
||||
componentTree String // JSON: full component tree
|
||||
level Int
|
||||
requiresAuth Boolean
|
||||
requiredRole String?
|
||||
@@ -62,76 +62,88 @@ model PageConfig {
|
||||
isPublished Boolean @default(true)
|
||||
params String?
|
||||
meta String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
components ComponentNode[]
|
||||
createdAt BigInt?
|
||||
updatedAt BigInt?
|
||||
|
||||
@@unique([tenantId, path])
|
||||
@@index([tenantId])
|
||||
@@index([packageId])
|
||||
@@index([level])
|
||||
@@index([isPublished])
|
||||
@@index([parentPath])
|
||||
}
|
||||
|
||||
model ComponentNode {
|
||||
id String @id @default(uuid())
|
||||
pageId String
|
||||
parentId String?
|
||||
type String
|
||||
childIds String
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
page PageConfig @relation(fields: [pageId], references: [id], onDelete: Cascade)
|
||||
parent ComponentNode? @relation("ParentChild", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children ComponentNode[] @relation("ParentChild")
|
||||
id String @id
|
||||
type String
|
||||
parentId String?
|
||||
childIds String // JSON: string[]
|
||||
order Int
|
||||
pageId String
|
||||
|
||||
@@index([pageId])
|
||||
@@index([parentId])
|
||||
@@index([pageId, order])
|
||||
}
|
||||
|
||||
model Workflow {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
description String?
|
||||
trigger String
|
||||
triggerConfig String
|
||||
steps String
|
||||
isActive Boolean @default(true)
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creator User @relation(fields: [createdBy], references: [id])
|
||||
|
||||
@@index([trigger])
|
||||
@@index([isActive])
|
||||
}
|
||||
|
||||
model LuaScript {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
description String?
|
||||
code String
|
||||
isSandboxed Boolean @default(true)
|
||||
allowedGlobals String
|
||||
timeoutMs Int @default(5000)
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creator User @relation(fields: [createdBy], references: [id])
|
||||
|
||||
@@index([isSandboxed])
|
||||
}
|
||||
|
||||
model InstalledPackage {
|
||||
packageId String @id
|
||||
|
||||
model ComponentConfig {
|
||||
id String @id
|
||||
componentId String
|
||||
props String // JSON
|
||||
styles String // JSON
|
||||
events String // JSON
|
||||
conditionalRendering String?
|
||||
|
||||
@@index([componentId])
|
||||
}
|
||||
|
||||
model Workflow {
|
||||
id String @id
|
||||
tenantId String?
|
||||
installedAt DateTime
|
||||
name String
|
||||
description String?
|
||||
nodes String // JSON: WorkflowNode[]
|
||||
edges String // JSON: WorkflowEdge[]
|
||||
enabled Boolean
|
||||
version Int @default(1)
|
||||
createdAt BigInt?
|
||||
updatedAt BigInt?
|
||||
createdBy String?
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([enabled])
|
||||
}
|
||||
|
||||
model LuaScript {
|
||||
id String @id
|
||||
tenantId String?
|
||||
name String
|
||||
description String?
|
||||
code String
|
||||
parameters String // JSON: Array<{name, type}>
|
||||
returnType String?
|
||||
isSandboxed Boolean @default(true)
|
||||
allowedGlobals String @default("[]")
|
||||
timeoutMs Int @default(5000)
|
||||
version Int @default(1)
|
||||
createdAt BigInt?
|
||||
updatedAt BigInt?
|
||||
createdBy String?
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
model InstalledPackage {
|
||||
packageId String @id
|
||||
tenantId String?
|
||||
installedAt BigInt
|
||||
version String
|
||||
enabled Boolean
|
||||
config String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
model PackageData {
|
||||
packageId String @id
|
||||
data String // JSON
|
||||
}
|
||||
|
||||
190
dbal/shared/tools/codegen/gen_prisma_schema.js
Normal file
190
dbal/shared/tools/codegen/gen_prisma_schema.js
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const header = `datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}`
|
||||
|
||||
const models = [
|
||||
{
|
||||
name: 'User',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'username', type: 'String', attributes: ['@unique'] },
|
||||
{ name: 'email', type: 'String', attributes: ['@unique'] },
|
||||
{ name: 'role', type: 'String' },
|
||||
{ name: 'profilePicture', type: 'String?' },
|
||||
{ name: 'bio', type: 'String?' },
|
||||
{ name: 'createdAt', type: 'BigInt' },
|
||||
{ name: 'tenantId', type: 'String?' },
|
||||
{ name: 'isInstanceOwner', type: 'Boolean', attributes: ['@default(false)'] },
|
||||
{ name: 'passwordChangeTimestamp', type: 'BigInt?' },
|
||||
{ name: 'firstLogin', type: 'Boolean', attributes: ['@default(false)'] },
|
||||
],
|
||||
blockAttributes: ['@@index([tenantId])', '@@index([role])'],
|
||||
},
|
||||
{
|
||||
name: 'Credential',
|
||||
fields: [
|
||||
{ name: 'username', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'passwordHash', type: 'String' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Session',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'userId', type: 'String' },
|
||||
{ name: 'token', type: 'String', attributes: ['@unique'] },
|
||||
{ name: 'expiresAt', type: 'BigInt' },
|
||||
{ name: 'createdAt', type: 'BigInt' },
|
||||
{ name: 'lastActivity', type: 'BigInt' },
|
||||
{ name: 'ipAddress', type: 'String?' },
|
||||
{ name: 'userAgent', type: 'String?' },
|
||||
],
|
||||
blockAttributes: ['@@index([userId])', '@@index([expiresAt])', '@@index([token])'],
|
||||
},
|
||||
{
|
||||
name: 'PageConfig',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'tenantId', type: 'String?' },
|
||||
{ name: 'packageId', type: 'String?' },
|
||||
{ name: 'path', type: 'String', comment: '// Route pattern: /media/jobs, /forum/:id' },
|
||||
{ name: 'title', type: 'String' },
|
||||
{ name: 'description', type: 'String?' },
|
||||
{ name: 'icon', type: 'String?' },
|
||||
{ name: 'component', type: 'String?' },
|
||||
{ name: 'componentTree', type: 'String', comment: '// JSON: full component tree' },
|
||||
{ name: 'level', type: 'Int' },
|
||||
{ name: 'requiresAuth', type: 'Boolean' },
|
||||
{ name: 'requiredRole', type: 'String?' },
|
||||
{ name: 'parentPath', type: 'String?' },
|
||||
{ name: 'sortOrder', type: 'Int', attributes: ['@default(0)'] },
|
||||
{ name: 'isPublished', type: 'Boolean', attributes: ['@default(true)'] },
|
||||
{ name: 'params', type: 'String?' },
|
||||
{ name: 'meta', type: 'String?' },
|
||||
{ name: 'createdAt', type: 'BigInt?' },
|
||||
{ name: 'updatedAt', type: 'BigInt?' },
|
||||
],
|
||||
blockAttributes: [
|
||||
'@@unique([tenantId, path])',
|
||||
'@@index([tenantId])',
|
||||
'@@index([packageId])',
|
||||
'@@index([level])',
|
||||
'@@index([parentPath])',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ComponentNode',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'type', type: 'String' },
|
||||
{ name: 'parentId', type: 'String?' },
|
||||
{ name: 'childIds', type: 'String', comment: '// JSON: string[]' },
|
||||
{ name: 'order', type: 'Int' },
|
||||
{ name: 'pageId', type: 'String' },
|
||||
],
|
||||
blockAttributes: ['@@index([pageId])', '@@index([parentId])'],
|
||||
},
|
||||
{
|
||||
name: 'ComponentConfig',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'componentId', type: 'String' },
|
||||
{ name: 'props', type: 'String', comment: '// JSON' },
|
||||
{ name: 'styles', type: 'String', comment: '// JSON' },
|
||||
{ name: 'events', type: 'String', comment: '// JSON' },
|
||||
{ name: 'conditionalRendering', type: 'String?' },
|
||||
],
|
||||
blockAttributes: ['@@index([componentId])'],
|
||||
},
|
||||
{
|
||||
name: 'Workflow',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'tenantId', type: 'String?' },
|
||||
{ name: 'name', type: 'String' },
|
||||
{ name: 'description', type: 'String?' },
|
||||
{ name: 'nodes', type: 'String', comment: '// JSON: WorkflowNode[]' },
|
||||
{ name: 'edges', type: 'String', comment: '// JSON: WorkflowEdge[]' },
|
||||
{ name: 'enabled', type: 'Boolean' },
|
||||
{ name: 'version', type: 'Int', attributes: ['@default(1)'] },
|
||||
{ name: 'createdAt', type: 'BigInt?' },
|
||||
{ name: 'updatedAt', type: 'BigInt?' },
|
||||
{ name: 'createdBy', type: 'String?' },
|
||||
],
|
||||
blockAttributes: ['@@index([tenantId])', '@@index([enabled])'],
|
||||
},
|
||||
{
|
||||
name: 'LuaScript',
|
||||
fields: [
|
||||
{ name: 'id', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'tenantId', type: 'String?' },
|
||||
{ name: 'name', type: 'String' },
|
||||
{ name: 'description', type: 'String?' },
|
||||
{ name: 'code', type: 'String' },
|
||||
{ name: 'parameters', type: 'String', comment: '// JSON: Array<{name, type}>' },
|
||||
{ name: 'returnType', type: 'String?' },
|
||||
{ name: 'isSandboxed', type: 'Boolean', attributes: ['@default(true)'] },
|
||||
{ name: 'allowedGlobals', type: 'String', attributes: ['@default("[]")'] },
|
||||
{ name: 'timeoutMs', type: 'Int', attributes: ['@default(5000)'] },
|
||||
{ name: 'version', type: 'Int', attributes: ['@default(1)'] },
|
||||
{ name: 'createdAt', type: 'BigInt?' },
|
||||
{ name: 'updatedAt', type: 'BigInt?' },
|
||||
{ name: 'createdBy', type: 'String?' },
|
||||
],
|
||||
blockAttributes: ['@@index([tenantId])', '@@index([name])'],
|
||||
},
|
||||
{
|
||||
name: 'InstalledPackage',
|
||||
fields: [
|
||||
{ name: 'packageId', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'tenantId', type: 'String?' },
|
||||
{ name: 'installedAt', type: 'BigInt' },
|
||||
{ name: 'version', type: 'String' },
|
||||
{ name: 'enabled', type: 'Boolean' },
|
||||
{ name: 'config', type: 'String?' },
|
||||
],
|
||||
blockAttributes: ['@@index([tenantId])'],
|
||||
},
|
||||
{
|
||||
name: 'PackageData',
|
||||
fields: [
|
||||
{ name: 'packageId', type: 'String', attributes: ['@id'] },
|
||||
{ name: 'data', type: 'String', comment: '// JSON' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const renderField = (field) => {
|
||||
const attrs = field.attributes ? ` ${field.attributes.join(' ')}` : ''
|
||||
const comment = field.comment ? ` ${field.comment}` : ''
|
||||
return ` ${field.name} ${field.type}${attrs}${comment}`
|
||||
}
|
||||
|
||||
const renderModel = (model) => {
|
||||
const lines = [`model ${model.name} {`]
|
||||
model.fields.forEach((field) => {
|
||||
lines.push(renderField(field))
|
||||
})
|
||||
if (model.blockAttributes) {
|
||||
model.blockAttributes.forEach((attr) => {
|
||||
lines.push(` ${attr}`)
|
||||
})
|
||||
}
|
||||
lines.push('}')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const schema = [header, models.map(renderModel).join('\n\n')].join('\n\n')
|
||||
const outputPath = path.resolve(__dirname, '../../backends/prisma/schema.prisma')
|
||||
fs.writeFileSync(outputPath, schema + '\n', 'utf8')
|
||||
console.log(`Prisma schema written to ${outputPath}`)
|
||||
Reference in New Issue
Block a user