T4/Phase 2C-1: port search routes to Hono (3 routes) + new DB tables
This commit is contained in:
+27
-1
@@ -10,6 +10,19 @@ import { taskRoutes } from "./routes/tasks";
|
|||||||
import { habitRoutes } from "./routes/habits";
|
import { habitRoutes } from "./routes/habits";
|
||||||
import { projectRoutes } from "./routes/projects";
|
import { projectRoutes } from "./routes/projects";
|
||||||
import { noteRoutes } from "./routes/notes";
|
import { noteRoutes } from "./routes/notes";
|
||||||
|
import { searchRoutes } from "./routes/search";
|
||||||
|
import { calendarRoutes } from "./routes/calendar";
|
||||||
|
import { graphRoutes } from "./routes/graph";
|
||||||
|
import { dashboardRoutes } from "./routes/dashboard";
|
||||||
|
import { agentRoutes } from "./routes/agents";
|
||||||
|
import { webhookRoutes } from "./routes/webhooks";
|
||||||
|
import { canvasRoutes } from "./routes/canvas";
|
||||||
|
import { dailyNoteRoutes } from "./routes/daily-notes";
|
||||||
|
import { tagRoutes } from "./routes/tags";
|
||||||
|
import { customFieldRoutes } from "./routes/custom-fields";
|
||||||
|
import { errorLogRoutes } from "./routes/error-log";
|
||||||
|
import { analyticsRoutes } from "./routes/analytics";
|
||||||
|
import { importExportRoutes } from "./routes/import-export";
|
||||||
import { healthHandler } from "./routes/health";
|
import { healthHandler } from "./routes/health";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -32,6 +45,19 @@ app.route("/api/tasks", taskRoutes);
|
|||||||
app.route("/api/habits", habitRoutes);
|
app.route("/api/habits", habitRoutes);
|
||||||
app.route("/api/projects", projectRoutes);
|
app.route("/api/projects", projectRoutes);
|
||||||
app.route("/api/notes", noteRoutes);
|
app.route("/api/notes", noteRoutes);
|
||||||
|
app.route("/api/search", searchRoutes);
|
||||||
|
app.route("/api/calendar", calendarRoutes);
|
||||||
|
app.route("/api/graph", graphRoutes);
|
||||||
|
app.route("/api/dashboard", dashboardRoutes);
|
||||||
|
app.route("/api/agents", agentRoutes);
|
||||||
|
app.route("/api/webhooks", webhookRoutes);
|
||||||
|
app.route("/api/canvas", canvasRoutes);
|
||||||
|
app.route("/api/daily-notes", dailyNoteRoutes);
|
||||||
|
app.route("/api/tags", tagRoutes);
|
||||||
|
app.route("/api/custom-fields", customFieldRoutes);
|
||||||
|
app.route("/api/error-log", errorLogRoutes);
|
||||||
|
app.route("/api/analytics", analyticsRoutes);
|
||||||
|
app.route("/api", importExportRoutes);
|
||||||
app.route("/api", realtimeRoutes);
|
app.route("/api", realtimeRoutes);
|
||||||
app.route("/mcp", mcpRoutes);
|
app.route("/mcp", mcpRoutes);
|
||||||
|
|
||||||
@@ -42,4 +68,4 @@ export default {
|
|||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`API server listening on :${port}`);
|
console.log("API server listening on :" + port);
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { db, sql } from "@project-e/db";
|
||||||
|
import { requireAuth, AuthError } from "../middleware/auth";
|
||||||
|
|
||||||
|
export const searchRoutes = new Hono();
|
||||||
|
|
||||||
|
const entityConfigs: Record<string, { table: string; titleColumn: string; contentColumn: string | null; linkPrefix: string; workspaceColumn: string; deletedColumn: string | null }> = {
|
||||||
|
task: { table: 'tasks', titleColumn: 'title', contentColumn: 'description', linkPrefix: '/tasks', workspaceColumn: 'domain_id', deletedColumn: 'deleted_at' },
|
||||||
|
note: { table: 'notes', titleColumn: 'title', contentColumn: 'content', linkPrefix: '/notes', workspaceColumn: 'domain_id', deletedColumn: 'deleted_at' },
|
||||||
|
project: { table: 'projects', titleColumn: 'name', contentColumn: 'description', linkPrefix: '/projects', workspaceColumn: 'domain_id', deletedColumn: 'deleted_at' },
|
||||||
|
habit: { table: 'habits', titleColumn: 'name', contentColumn: 'description', linkPrefix: '/habits', workspaceColumn: 'domain_id', deletedColumn: 'deleted_at' },
|
||||||
|
domain: { table: 'domains', titleColumn: 'name', contentColumn: null, linkPrefix: '/settings', workspaceColumn: 'id', deletedColumn: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET /api/search?q=...&type=... — Cross-entity full-text search
|
||||||
|
searchRoutes.get("/", async (c) => {
|
||||||
|
try {
|
||||||
|
const user = await requireAuth(c);
|
||||||
|
const url = new URL(c.req.url);
|
||||||
|
const q = (url.searchParams.get('q') || '').trim();
|
||||||
|
const types = url.searchParams.get('types')?.split(',').filter(Boolean) || ['task', 'note', 'project', 'habit', 'domain'];
|
||||||
|
const limit = Math.max(1, Math.min(50, parseInt(url.searchParams.get('limit') || '20')));
|
||||||
|
const offset = Math.max(0, parseInt(url.searchParams.get('offset') || '0'));
|
||||||
|
|
||||||
|
if (!q) {
|
||||||
|
return c.json({ results: [], totalCount: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = q.replace(/['"\\]/g, '').trim();
|
||||||
|
if (!sanitized) {
|
||||||
|
return c.json({ results: [], totalCount: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Array<{ id: string; type: string; title: string; snippet: string; score: number; workspaceId: string; link: string }> = [];
|
||||||
|
|
||||||
|
for (const type of types) {
|
||||||
|
const config = entityConfigs[type];
|
||||||
|
if (!config) continue;
|
||||||
|
|
||||||
|
const { table, titleColumn, contentColumn, linkPrefix, workspaceColumn, deletedColumn } = config;
|
||||||
|
const escaped = sanitized.replace(/'/g, "''");
|
||||||
|
const conditions: string[] = ["search_vector @@ websearch_to_tsquery('english', '" + escaped + "')"];
|
||||||
|
if (deletedColumn) {
|
||||||
|
conditions.push(deletedColumn + " IS NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.join(' AND ');
|
||||||
|
const headlineColumn = contentColumn || titleColumn;
|
||||||
|
|
||||||
|
const queryStr = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
${titleColumn} AS title,
|
||||||
|
ts_headline('english', ${headlineColumn}, websearch_to_tsquery('english', '${escaped}'),
|
||||||
|
'MaxWords=30, MinWords=15, ShortWord=3, HighlightAll=FALSE, StartSel=<mark>, StopSel=</mark>, FragmentDelimiter=...'
|
||||||
|
) AS snippet,
|
||||||
|
ts_rank(search_vector, websearch_to_tsquery('english', '${escaped}')) AS score,
|
||||||
|
${workspaceColumn} AS workspace_id
|
||||||
|
FROM ${table}
|
||||||
|
WHERE ${whereClause}
|
||||||
|
ORDER BY score DESC
|
||||||
|
LIMIT 50
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rows: any[] = await sql.unsafe(queryStr);
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
results.push({
|
||||||
|
id: String(row.id),
|
||||||
|
type,
|
||||||
|
title: String(row.title || ''),
|
||||||
|
snippet: String(row.snippet || ''),
|
||||||
|
score: Number(row.score || 0),
|
||||||
|
workspaceId: String(row.workspace_id || ''),
|
||||||
|
link: linkPrefix + '/' + row.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.sort((a, b) => b.score - a.score);
|
||||||
|
const totalCount = results.length;
|
||||||
|
const paginated = results.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
return c.json({ results: paginated, totalCount, query: q });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return c.json({ error: { code: error.code, message: error.message } }, error.status as any);
|
||||||
|
}
|
||||||
|
console.error('[search] GET error:', error);
|
||||||
|
return c.json({ error: { code: 'INTERNAL_ERROR', message: 'Search failed' } }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/search/recent — Recent searches (stub)
|
||||||
|
searchRoutes.get("/recent", async (c) => {
|
||||||
|
try {
|
||||||
|
await requireAuth(c);
|
||||||
|
return c.json({ items: [], totalItems: 0 });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return c.json({ error: { code: error.code, message: error.message } }, error.status as any);
|
||||||
|
}
|
||||||
|
console.error('[search] GET /recent error:', error);
|
||||||
|
return c.json({ error: { code: 'INTERNAL_ERROR', message: 'Failed to get recent searches' } }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/search/index — Reindex (admin stub)
|
||||||
|
searchRoutes.post("/index", async (c) => {
|
||||||
|
try {
|
||||||
|
await requireAuth(c);
|
||||||
|
return c.json({ success: true, message: 'Reindex triggered' });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return c.json({ error: { code: error.code, message: error.message } }, error.status as any);
|
||||||
|
}
|
||||||
|
console.error('[search] POST /index error:', error);
|
||||||
|
return c.json({ error: { code: 'INTERNAL_ERROR', message: 'Failed to reindex' } }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -528,3 +528,280 @@ export const apiKeys = pgTable(
|
|||||||
index('api_keys_active_idx').on(table.active),
|
index('api_keys_active_idx').on(table.active),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Agents ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const agents = pgTable(
|
||||||
|
'agents',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
status: text('status').notNull().default('active'),
|
||||||
|
permissionTier: text('permission_tier').notNull().default('read_only'),
|
||||||
|
customPermissions: text('custom_permissions').array().default([]),
|
||||||
|
apiKey: text('api_key'),
|
||||||
|
lastActiveAt: timestamp('last_active_at', { withTimezone: true }),
|
||||||
|
domainId: uuid('domain_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => domains.id, { onDelete: 'cascade' }),
|
||||||
|
tags: text('tags').array().default([]),
|
||||||
|
config: jsonb('config').$type<Record<string, unknown>>().default({}),
|
||||||
|
customFields: jsonb('custom_fields').$type<Record<string, unknown>>().default({}),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('agents_domain_id_idx').on(table.domainId),
|
||||||
|
index('agents_status_idx').on(table.status),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Agent Activity ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const agentActivity = pgTable(
|
||||||
|
'agent_activity',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
agentId: uuid('agent_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => agents.id, { onDelete: 'cascade' }),
|
||||||
|
action: text('action').notNull(),
|
||||||
|
entityType: text('entity_type'),
|
||||||
|
entityId: uuid('entity_id'),
|
||||||
|
details: jsonb('details').$type<Record<string, unknown>>().default({}),
|
||||||
|
success: boolean('success').default(true),
|
||||||
|
errorMessage: text('error_message'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('agent_activity_agent_id_idx').on(table.agentId),
|
||||||
|
index('agent_activity_created_at_idx').on(table.createdAt),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Agent Tasks ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const agentTasks = pgTable(
|
||||||
|
'agent_tasks',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
agentId: uuid('agent_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => agents.id, { onDelete: 'cascade' }),
|
||||||
|
taskType: text('task_type').notNull(),
|
||||||
|
input: jsonb('input').$type<Record<string, unknown>>().default({}),
|
||||||
|
status: text('status').notNull().default('pending'),
|
||||||
|
output: jsonb('output').$type<Record<string, unknown>>(),
|
||||||
|
errorMessage: text('error_message'),
|
||||||
|
startedAt: timestamp('started_at', { withTimezone: true }),
|
||||||
|
completedAt: timestamp('completed_at', { withTimezone: true }),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('agent_tasks_agent_id_idx').on(table.agentId),
|
||||||
|
index('agent_tasks_status_idx').on(table.status),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Canvases ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const canvases = pgTable(
|
||||||
|
'canvases',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
mode: text('mode').notNull().default('freeform'),
|
||||||
|
domainId: uuid('domain_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => domains.id, { onDelete: 'cascade' }),
|
||||||
|
tags: text('tags').array().default([]),
|
||||||
|
viewport: jsonb('viewport').$type<{ x: number; y: number; zoom: number }>().default({ x: 0, y: 0, zoom: 1 }),
|
||||||
|
background: text('background'),
|
||||||
|
customFields: jsonb('custom_fields').$type<Record<string, unknown>>().default({}),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('canvases_domain_id_idx').on(table.domainId),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Canvas Cards ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const canvasCards = pgTable(
|
||||||
|
'canvas_cards',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
canvasId: uuid('canvas_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => canvases.id, { onDelete: 'cascade' }),
|
||||||
|
type: text('type').notNull().default('note'),
|
||||||
|
entityId: uuid('entity_id'),
|
||||||
|
title: text('title'),
|
||||||
|
content: text('content'),
|
||||||
|
x: integer('x').default(0),
|
||||||
|
y: integer('y').default(0),
|
||||||
|
width: integer('width').default(200),
|
||||||
|
height: integer('height').default(150),
|
||||||
|
rotation: integer('rotation').default(0),
|
||||||
|
color: text('color'),
|
||||||
|
zIndex: integer('z_index').default(0),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('canvas_cards_canvas_id_idx').on(table.canvasId),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Canvas Connections ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const canvasConnections = pgTable(
|
||||||
|
'canvas_connections',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
canvasId: uuid('canvas_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => canvases.id, { onDelete: 'cascade' }),
|
||||||
|
sourceCardId: uuid('source_card_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => canvasCards.id, { onDelete: 'cascade' }),
|
||||||
|
targetCardId: uuid('target_card_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => canvasCards.id, { onDelete: 'cascade' }),
|
||||||
|
label: text('label'),
|
||||||
|
style: text('style').notNull().default('solid'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('canvas_connections_canvas_id_idx').on(table.canvasId),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Calendar Events ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const calendarEvents = pgTable(
|
||||||
|
'calendar_events',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
title: text('title').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
startTime: timestamp('start_time', { withTimezone: true }).notNull(),
|
||||||
|
endTime: timestamp('end_time', { withTimezone: true }),
|
||||||
|
allDay: boolean('all_day').default(false),
|
||||||
|
color: text('color'),
|
||||||
|
domainId: uuid('domain_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => domains.id, { onDelete: 'cascade' }),
|
||||||
|
entityType: text('entity_type'),
|
||||||
|
entityId: uuid('entity_id'),
|
||||||
|
recurrenceRule: text('recurrence_rule'),
|
||||||
|
customFields: jsonb('custom_fields').$type<Record<string, unknown>>().default({}),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('calendar_events_domain_id_idx').on(table.domainId),
|
||||||
|
index('calendar_events_start_time_idx').on(table.startTime),
|
||||||
|
index('calendar_events_end_time_idx').on(table.endTime),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Dashboard Widgets ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const dashboardWidgets = pgTable(
|
||||||
|
'dashboard_widgets',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
userId: uuid('user_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => users.id, { onDelete: 'cascade' }),
|
||||||
|
type: text('type').notNull(),
|
||||||
|
title: text('title'),
|
||||||
|
config: jsonb('config').$type<Record<string, unknown>>().default({}),
|
||||||
|
layout: jsonb('layout').$type<{ x: number; y: number; w: number; h: number }>().default({ x: 0, y: 0, w: 2, h: 2 }),
|
||||||
|
domainId: uuid('domain_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => domains.id, { onDelete: 'cascade' }),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('dashboard_widgets_user_id_idx').on(table.userId),
|
||||||
|
index('dashboard_widgets_domain_id_idx').on(table.domainId),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Daily Notes ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const dailyNotes = pgTable(
|
||||||
|
'daily_notes',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
date: timestamp('date', { withTimezone: true }).notNull(),
|
||||||
|
content: text('content'),
|
||||||
|
domainId: uuid('domain_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => domains.id, { onDelete: 'cascade' }),
|
||||||
|
mood: integer('mood'),
|
||||||
|
energy: integer('energy'),
|
||||||
|
customFields: jsonb('custom_fields').$type<Record<string, unknown>>().default({}),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('daily_notes_domain_id_idx').on(table.domainId),
|
||||||
|
index('daily_notes_date_idx').on(table.date),
|
||||||
|
uniqueIndex('daily_notes_date_domain_idx').on(table.date, table.domainId),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Custom Fields ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const customFields = pgTable(
|
||||||
|
'custom_fields',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
type: text('type').notNull().default('text'),
|
||||||
|
entityType: text('entity_type').notNull(),
|
||||||
|
domainId: uuid('domain_id')
|
||||||
|
.notNull()
|
||||||
|
.references((): any => domains.id, { onDelete: 'cascade' }),
|
||||||
|
required: boolean('required').default(false),
|
||||||
|
options: jsonb('options').$type<string[]>().default([]),
|
||||||
|
defaultValue: jsonb('default_value'),
|
||||||
|
sortOrder: integer('sort_order').default(0),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('custom_fields_domain_id_idx').on(table.domainId),
|
||||||
|
index('custom_fields_entity_type_idx').on(table.entityType),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Error Logs ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const errorLogs = pgTable(
|
||||||
|
'error_logs',
|
||||||
|
{
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
level: text('level').notNull().default('error'),
|
||||||
|
source: text('source').notNull(),
|
||||||
|
message: text('message').notNull(),
|
||||||
|
stackTrace: text('stack_trace'),
|
||||||
|
metadata: jsonb('metadata').$type<Record<string, unknown>>().default({}),
|
||||||
|
resolved: boolean('resolved').default(false),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('error_logs_level_idx').on(table.level),
|
||||||
|
index('error_logs_created_at_idx').on(table.createdAt),
|
||||||
|
index('error_logs_resolved_idx').on(table.resolved),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user