a3921a0b98
- Add `ZEN_DB_SSL_DISABLED` env variable to allow disabling SSL for database connections - Refactor database CLI to split init logic into `initFeatures` and `initModules` for modular table initialization, with graceful fallback when modules are absent - Extract `printHelp` and `askConfirmation` helpers for cleaner CLI structure - Ensure `closePool` is called on both success and error paths in CLI - Add `filterAllowedColumns` utility in `crud.js` to enforce column whitelists, preventing mass-assignment of privileged fields (e.g. `role`, `email_verified`) - Update drop command description from "auth tables" to "all tables"
130 lines
4.2 KiB
JavaScript
130 lines
4.2 KiB
JavaScript
/**
|
|
* Posts Module - Database
|
|
* Creates zen_posts and zen_posts_category tables.
|
|
*/
|
|
|
|
import { query, tableExists } from '@zen/core/database';
|
|
import { getPostsConfig } from './config.js';
|
|
import { done, info, step } from '../../shared/lib/logger.js';
|
|
|
|
async function createPostsCategoryTable() {
|
|
const tableName = 'zen_posts_category';
|
|
const exists = await tableExists(tableName);
|
|
|
|
if (exists) {
|
|
info(`Table already exists: ${tableName}`);
|
|
return { created: false, tableName };
|
|
}
|
|
|
|
await query(`
|
|
CREATE TABLE zen_posts_category (
|
|
id SERIAL PRIMARY KEY,
|
|
post_type VARCHAR(100) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
await query(`CREATE INDEX idx_zen_posts_category_post_type ON zen_posts_category(post_type)`);
|
|
await query(`CREATE INDEX idx_zen_posts_category_is_active ON zen_posts_category(is_active)`);
|
|
|
|
done(`Created table: ${tableName}`);
|
|
return { created: true, tableName };
|
|
}
|
|
|
|
async function createPostsTable() {
|
|
const tableName = 'zen_posts';
|
|
const exists = await tableExists(tableName);
|
|
|
|
if (exists) {
|
|
info(`Table already exists: ${tableName}`);
|
|
return { created: false, tableName };
|
|
}
|
|
|
|
await query(`
|
|
CREATE TABLE zen_posts (
|
|
id SERIAL PRIMARY KEY,
|
|
post_type VARCHAR(100) NOT NULL,
|
|
slug VARCHAR(500) NOT NULL,
|
|
data JSONB NOT NULL DEFAULT '{}',
|
|
category_id INTEGER REFERENCES zen_posts_category(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(post_type, slug)
|
|
)
|
|
`);
|
|
|
|
await query(`CREATE INDEX idx_zen_posts_post_type ON zen_posts(post_type)`);
|
|
await query(`CREATE INDEX idx_zen_posts_post_type_slug ON zen_posts(post_type, slug)`);
|
|
await query(`CREATE INDEX idx_zen_posts_category_id ON zen_posts(category_id)`);
|
|
await query(`CREATE INDEX idx_zen_posts_data_gin ON zen_posts USING GIN (data)`);
|
|
|
|
done(`Created table: ${tableName}`);
|
|
return { created: true, tableName };
|
|
}
|
|
|
|
async function createPostsRelationsTable() {
|
|
const tableName = 'zen_posts_relations';
|
|
const exists = await tableExists(tableName);
|
|
|
|
if (exists) {
|
|
info(`Table already exists: ${tableName}`);
|
|
return { created: false, tableName };
|
|
}
|
|
|
|
await query(`
|
|
CREATE TABLE zen_posts_relations (
|
|
id SERIAL PRIMARY KEY,
|
|
post_id INTEGER NOT NULL REFERENCES zen_posts(id) ON DELETE CASCADE,
|
|
field_name VARCHAR(100) NOT NULL,
|
|
related_post_id INTEGER NOT NULL REFERENCES zen_posts(id) ON DELETE CASCADE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
UNIQUE(post_id, field_name, related_post_id)
|
|
)
|
|
`);
|
|
|
|
await query(`CREATE INDEX idx_zen_posts_relations_post_id ON zen_posts_relations(post_id)`);
|
|
await query(`CREATE INDEX idx_zen_posts_relations_related ON zen_posts_relations(related_post_id)`);
|
|
|
|
done(`Created table: ${tableName}`);
|
|
return { created: true, tableName };
|
|
}
|
|
|
|
/**
|
|
* Create all posts-related tables.
|
|
* zen_posts_category is only created if at least one type uses the 'category' field.
|
|
* zen_posts_relations is only created if at least one type uses the 'relation' field.
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
export async function createTables() {
|
|
const created = [];
|
|
const skipped = [];
|
|
|
|
const config = getPostsConfig();
|
|
const needsRelations = Object.values(config.types).some(t => t.hasRelations);
|
|
|
|
// zen_posts_category must always be created before zen_posts
|
|
// because zen_posts has a FK reference to it
|
|
step('Posts Categories');
|
|
const catResult = await createPostsCategoryTable();
|
|
if (catResult.created) created.push(catResult.tableName);
|
|
else skipped.push(catResult.tableName);
|
|
|
|
step('Posts');
|
|
const postResult = await createPostsTable();
|
|
if (postResult.created) created.push(postResult.tableName);
|
|
else skipped.push(postResult.tableName);
|
|
|
|
if (needsRelations) {
|
|
step('Posts Relations');
|
|
const relResult = await createPostsRelationsTable();
|
|
if (relResult.created) created.push(relResult.tableName);
|
|
else skipped.push(relResult.tableName);
|
|
}
|
|
|
|
return { created, skipped };
|
|
}
|