refactor(pcd): reorganize regularExpressions to CRUD pattern

This commit is contained in:
Sam Chau
2026-01-27 22:12:01 +10:30
parent d6c4655608
commit dc837a5254
13 changed files with 111 additions and 105 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@ node_modules
CLAUDE.md
.claudeignore
# Local TODO/planning docs
docs/todo/
# Output
.output
.vercel

8
deno.lock generated
View File

@@ -4,10 +4,12 @@
"jsr:@denosaurs/plug@^1.1.0": "1.1.0",
"jsr:@felix/bcrypt@^1.0.8": "1.0.8",
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0": "2.2.0",
"jsr:@std/assert@1": "1.0.15",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/fmt@1": "1.0.8",
"jsr:@std/fs@1": "1.0.19",
"jsr:@std/internal@^1.0.10": "1.0.12",
"jsr:@std/internal@^1.0.12": "1.0.12",
"jsr:@std/internal@^1.0.9": "1.0.12",
"jsr:@std/path@1": "1.1.2",
"jsr:@std/path@^1.1.1": "1.1.2"
@@ -31,6 +33,12 @@
"@soapbox/kysely-deno-sqlite@2.2.0": {
"integrity": "668ec94600bc4b4d7bd618dd7ca65d4ef30ee61c46ffcb379b6f45203c08517a"
},
"@std/assert@1.0.15": {
"integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
"dependencies": [
"jsr:@std/internal@^1.0.12"
]
},
"@std/encoding@1.0.10": {
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
},

View File

@@ -5,7 +5,7 @@
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
export interface CreateRegularExpressionInput {
interface CreateRegularExpressionInput {
name: string;
pattern: string;
tags: string[];
@@ -13,7 +13,7 @@ export interface CreateRegularExpressionInput {
regex101Id: string | null;
}
export interface CreateRegularExpressionOptions {
interface CreateRegularExpressionOptions {
databaseId: number;
cache: PCDCache;
layer: OperationLayer;

View File

@@ -4,14 +4,14 @@
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { RegularExpressionTableRow } from './types.ts';
import type { RegularExpressionWithTags } from '$shared/pcd/display.ts';
export interface DeleteRegularExpressionOptions {
interface DeleteRegularExpressionOptions {
databaseId: number;
cache: PCDCache;
layer: OperationLayer;
/** The current regular expression data (for value guards) */
current: RegularExpressionTableRow;
current: RegularExpressionWithTags;
}
/**

View File

@@ -1,37 +0,0 @@
/**
* Get a single regular expression by ID
*/
import type { PCDCache } from '../../cache.ts';
import type { RegularExpressionTableRow } from './types.ts';
/**
* Get a regular expression by ID with its tags
*/
export async function get(cache: PCDCache, id: number): Promise<RegularExpressionTableRow | null> {
const db = cache.kb;
// Get the regular expression
const regex = await db
.selectFrom('regular_expressions')
.select(['id', 'name', 'pattern', 'regex101_id', 'description', 'created_at', 'updated_at'])
.where('id', '=', id)
.executeTakeFirst();
if (!regex) {
return null;
}
// Get tags for this regular expression
const tags = await db
.selectFrom('regular_expression_tags as ret')
.innerJoin('tags as t', 't.name', 'ret.tag_name')
.select(['t.name', 't.created_at'])
.where('ret.regular_expression_name', '=', regex.name)
.execute();
return {
...regex,
tags
};
}

View File

@@ -1,17 +1,15 @@
/**
* Regular Expression queries and mutations
* Regular Expression CRUD operations
*/
// Export all types
export type { RegularExpressionTableRow } from './types.ts';
export type { CreateRegularExpressionInput } from './create.ts';
export type { UpdateRegularExpressionInput } from './update.ts';
// Read
export { list, get } from './read.ts';
// Export query functions
export { list } from './list.ts';
export { get } from './get.ts';
// Export mutation functions
// Create
export { create } from './create.ts';
// Update
export { update } from './update.ts';
// Delete
export { remove } from './delete.ts';

View File

@@ -1,18 +1,17 @@
/**
* Regular expression list queries
* Regular expression read operations
*/
import type { PCDCache } from '../../cache.ts';
import type { Tag } from '../../types.ts';
import type { RegularExpressionTableRow } from './types.ts';
import type { Tag, RegularExpressionWithTags } from '$shared/pcd/display.ts';
/**
* Get regular expressions with full data for table/card views
* List all regular expressions with tags
*/
export async function list(cache: PCDCache): Promise<RegularExpressionTableRow[]> {
export async function list(cache: PCDCache): Promise<RegularExpressionWithTags[]> {
const db = cache.kb;
// 1. Get all regular expressions
// Get all regular expressions
const expressions = await db
.selectFrom('regular_expressions')
.select(['id', 'name', 'pattern', 'regex101_id', 'description', 'created_at', 'updated_at'])
@@ -23,7 +22,7 @@ export async function list(cache: PCDCache): Promise<RegularExpressionTableRow[]
const expressionNames = expressions.map((e) => e.name);
// 2. Get all tags for all expressions
// Get all tags for all expressions
const allTags = await db
.selectFrom('regular_expression_tags as ret')
.innerJoin('tags as t', 't.name', 'ret.tag_name')
@@ -47,13 +46,38 @@ export async function list(cache: PCDCache): Promise<RegularExpressionTableRow[]
// Build the final result
return expressions.map((expression) => ({
id: expression.id,
name: expression.name,
pattern: expression.pattern,
regex101_id: expression.regex101_id,
description: expression.description,
tags: tagsMap.get(expression.name) || [],
created_at: expression.created_at,
updated_at: expression.updated_at
...expression,
tags: tagsMap.get(expression.name) || []
}));
}
/**
* Get a single regular expression by ID with tags
*/
export async function get(cache: PCDCache, id: number): Promise<RegularExpressionWithTags | null> {
const db = cache.kb;
// Get the regular expression
const regex = await db
.selectFrom('regular_expressions')
.select(['id', 'name', 'pattern', 'regex101_id', 'description', 'created_at', 'updated_at'])
.where('id', '=', id)
.executeTakeFirst();
if (!regex) {
return null;
}
// Get tags for this regular expression
const tags = await db
.selectFrom('regular_expression_tags as ret')
.innerJoin('tags as t', 't.name', 'ret.tag_name')
.select(['t.name', 't.created_at'])
.where('ret.regular_expression_name', '=', regex.name)
.execute();
return {
...regex,
tags
};
}

View File

@@ -1,17 +0,0 @@
/**
* Regular Expression query-specific types
*/
import type { Tag } from '../../types.ts';
/** Regular expression data for table/card views */
export interface RegularExpressionTableRow {
id: number;
name: string;
pattern: string;
regex101_id: string | null;
description: string | null;
tags: Tag[];
created_at: string;
updated_at: string;
}

View File

@@ -4,10 +4,10 @@
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { RegularExpressionTableRow } from './types.ts';
import type { RegularExpressionWithTags } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';
export interface UpdateRegularExpressionInput {
interface UpdateRegularExpressionInput {
name: string;
pattern: string;
tags: string[];
@@ -15,12 +15,12 @@ export interface UpdateRegularExpressionInput {
regex101Id: string | null;
}
export interface UpdateRegularExpressionOptions {
interface UpdateRegularExpressionOptions {
databaseId: number;
cache: PCDCache;
layer: OperationLayer;
/** The current regular expression data (for value guards) */
current: RegularExpressionTableRow;
current: RegularExpressionWithTags;
/** The new values */
input: UpdateRegularExpressionInput;
}

View File

@@ -0,0 +1,27 @@
/**
* PCD Display Types
*
* Types for query results that include JOINed data.
* These extend the generated Row types with related entities.
*/
import type { RegularExpressionsRow } from './types.ts';
// ============================================================================
// COMMON
// ============================================================================
/** Tag with metadata */
export interface Tag {
name: string;
created_at: string;
}
// ============================================================================
// REGULAR EXPRESSIONS
// ============================================================================
/** Regular expression with tags (from JOIN) */
export type RegularExpressionWithTags = RegularExpressionsRow & {
tags: Tag[];
};

View File

@@ -14,7 +14,7 @@
import { browser } from '$app/environment';
import { Info, Plus, FileText, Users } from 'lucide-svelte';
import { goto } from '$app/navigation';
import type { RegularExpressionTableRow } from '$pcd/queries/regularExpressions';
import type { RegularExpressionWithTags } from '$shared/pcd/display';
import type { PageData } from './$types';
export let data: PageData;
@@ -74,10 +74,10 @@
$: filtered = filterExpressions(data.regularExpressions, $debouncedQuery, searchOptions);
function filterExpressions(
items: RegularExpressionTableRow[],
items: RegularExpressionWithTags[],
query: string,
options: typeof searchOptions
): RegularExpressionTableRow[] {
): RegularExpressionWithTags[] {
if (!query) return items;
const queryLower = query.toLowerCase();
@@ -89,7 +89,7 @@
// Search within tag names
return item.tags.some((tag) => tag.name.toLowerCase().includes(queryLower));
}
const value = item[key as keyof RegularExpressionTableRow];
const value = item[key as keyof RegularExpressionWithTags];
if (value == null) return false;
return String(value).toLowerCase().includes(queryLower);
});

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import type { RegularExpressionTableRow } from '$pcd/queries/regularExpressions';
import type { RegularExpressionWithTags } from '$shared/pcd/display';
import { ExternalLink } from 'lucide-svelte';
import { marked } from 'marked';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
export let expressions: RegularExpressionTableRow[];
export let expressions: RegularExpressionWithTags[];
function handleCardClick(expression: RegularExpressionTableRow) {
function handleCardClick(expression: RegularExpressionWithTags) {
const databaseId = $page.params.databaseId;
goto(`/regular-expressions/${databaseId}/${expression.id}`);
}

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import Table from '$ui/table/Table.svelte';
import type { Column } from '$ui/table/types';
import type { RegularExpressionTableRow } from '$pcd/queries/regularExpressions';
import type { RegularExpressionWithTags } from '$shared/pcd/display';
import { Tag, Code, FileText, Link, Calendar } from 'lucide-svelte';
import { marked } from 'marked';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { parseUTC } from '$shared/dates';
export let expressions: RegularExpressionTableRow[];
export let expressions: RegularExpressionWithTags[];
function formatDate(dateString: string): string {
const date = parseUTC(dateString);
@@ -22,7 +22,7 @@
});
}
function handleRowClick(row: RegularExpressionTableRow) {
function handleRowClick(row: RegularExpressionWithTags) {
const databaseId = $page.params.databaseId;
goto(`/regular-expressions/${databaseId}/${row.id}`);
}
@@ -41,7 +41,7 @@
return marked.parseInline(text) as string;
}
const columns: Column<RegularExpressionTableRow>[] = [
const columns: Column<RegularExpressionWithTags>[] = [
{
key: 'name',
header: 'Name',
@@ -49,7 +49,7 @@
align: 'left',
sortable: true,
width: 'w-48',
cell: (row: RegularExpressionTableRow) => ({
cell: (row: RegularExpressionWithTags) => ({
html: `
<div>
<div class="font-medium">${escapeHtml(row.name)}</div>
@@ -79,7 +79,7 @@
header: 'Pattern',
headerIcon: Code,
align: 'left',
cell: (row: RegularExpressionTableRow) => ({
cell: (row: RegularExpressionWithTags) => ({
html: `<code class="font-mono text-xs bg-neutral-100 dark:bg-neutral-800 px-2 py-1 rounded break-all">${escapeHtml(row.pattern)}</code>`
})
},
@@ -88,7 +88,7 @@
header: 'Description',
headerIcon: FileText,
align: 'left',
cell: (row: RegularExpressionTableRow) => ({
cell: (row: RegularExpressionWithTags) => ({
html: row.description
? `<span class="text-sm text-neutral-600 dark:text-neutral-400 prose-inline">${parseMarkdown(row.description)}</span>`
: `<span class="text-neutral-400">-</span>`
@@ -100,7 +100,7 @@
headerIcon: Link,
align: 'left',
width: 'w-32',
cell: (row: RegularExpressionTableRow) => ({
cell: (row: RegularExpressionWithTags) => ({
html: row.regex101_id
? `<a href="https://regex101.com/r/${escapeHtml(row.regex101_id)}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1 font-mono text-xs text-accent-600 hover:text-accent-700 dark:text-accent-400 dark:hover:text-accent-300 hover:underline">${escapeHtml(row.regex101_id)}<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a>`
: `<span class="text-neutral-400">-</span>`
@@ -113,7 +113,7 @@
align: 'left',
width: 'w-44',
sortable: true,
cell: (row: RegularExpressionTableRow) => ({
cell: (row: RegularExpressionWithTags) => ({
html: `<span class="text-xs text-neutral-500 dark:text-neutral-400">${formatDate(row.updated_at)}</span>`
})
},
@@ -124,7 +124,7 @@
align: 'left',
width: 'w-44',
sortable: true,
cell: (row: RegularExpressionTableRow) => ({
cell: (row: RegularExpressionWithTags) => ({
html: `<span class="text-xs text-neutral-500 dark:text-neutral-400">${formatDate(row.created_at)}</span>`
})
}