hazo_connect 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ /* globals.css defines base Tailwind layers and root-level tokens for hazo_connect. */
2
+ @tailwind base;
3
+ @tailwind components;
4
+ @tailwind utilities;
5
+
6
+ /* base_layer sets foundational styles shared across the library. */
7
+ :root {
8
+ color-scheme: light;
9
+ }
10
+
11
+ /* body_reset normalizes default margins and background color. */
12
+ body {
13
+ margin: 0;
14
+ background-color: #f8fafc;
15
+ }
16
+
@@ -0,0 +1,188 @@
1
+ import { NextRequest, NextResponse } from "next/server"
2
+ import {
3
+ getSqliteAdminService,
4
+ type RowQueryOptions,
5
+ type SqliteFilterOperator,
6
+ type SqliteWhereFilter
7
+ } from "hazo_connect"
8
+
9
+ export const dynamic = "force-dynamic"
10
+ const allowedOperators: SqliteFilterOperator[] = [
11
+ "eq",
12
+ "neq",
13
+ "gt",
14
+ "gte",
15
+ "lt",
16
+ "lte",
17
+ "like",
18
+ "ilike",
19
+ "is"
20
+ ]
21
+
22
+ export async function GET(request: NextRequest) {
23
+ const url = new URL(request.url)
24
+ const table = url.searchParams.get("table")
25
+ if (!table) {
26
+ return NextResponse.json(
27
+ { error: "Query parameter 'table' is required." },
28
+ { status: 400 }
29
+ )
30
+ }
31
+
32
+ try {
33
+ const service = getSqliteAdminService()
34
+ const options = parseRowQueryOptions(url.searchParams)
35
+ const page = await service.getTableData(table, options)
36
+ return NextResponse.json({ data: page.rows, total: page.total })
37
+ } catch (error) {
38
+ return toErrorResponse(error, `Failed to fetch data for table '${table}'`)
39
+ }
40
+ }
41
+
42
+ export async function POST(request: NextRequest) {
43
+ try {
44
+ const service = getSqliteAdminService()
45
+ const payload = await request.json()
46
+ const table = payload?.table
47
+ const data = payload?.data
48
+
49
+ if (!table || typeof data !== "object" || Array.isArray(data)) {
50
+ return NextResponse.json(
51
+ { error: "Request body must include 'table' and a 'data' object." },
52
+ { status: 400 }
53
+ )
54
+ }
55
+
56
+ const inserted = await service.insertRow(table, data)
57
+ return NextResponse.json({ data: inserted })
58
+ } catch (error) {
59
+ return toErrorResponse(error, "Failed to insert row")
60
+ }
61
+ }
62
+
63
+ export async function PATCH(request: NextRequest) {
64
+ try {
65
+ const service = getSqliteAdminService()
66
+ const payload = await request.json()
67
+ const table = payload?.table
68
+ const data = payload?.data
69
+ const criteria = payload?.criteria
70
+
71
+ if (
72
+ !table ||
73
+ typeof data !== "object" ||
74
+ Array.isArray(data) ||
75
+ typeof criteria !== "object" ||
76
+ criteria === null ||
77
+ Array.isArray(criteria)
78
+ ) {
79
+ return NextResponse.json(
80
+ {
81
+ error:
82
+ "Request body must include 'table', 'data' object, and a 'criteria' object for the rows to update."
83
+ },
84
+ { status: 400 }
85
+ )
86
+ }
87
+
88
+ const rows = await service.updateRows(table, criteria, data)
89
+ return NextResponse.json({ data: rows, updated: rows.length })
90
+ } catch (error) {
91
+ return toErrorResponse(error, "Failed to update rows")
92
+ }
93
+ }
94
+
95
+ export async function DELETE(request: NextRequest) {
96
+ try {
97
+ const service = getSqliteAdminService()
98
+ const payload = await request.json()
99
+ const table = payload?.table
100
+ const criteria = payload?.criteria
101
+
102
+ if (
103
+ !table ||
104
+ typeof criteria !== "object" ||
105
+ criteria === null ||
106
+ Array.isArray(criteria)
107
+ ) {
108
+ return NextResponse.json(
109
+ {
110
+ error:
111
+ "Request body must include 'table' and a 'criteria' object for the rows to delete."
112
+ },
113
+ { status: 400 }
114
+ )
115
+ }
116
+
117
+ const rows = await service.deleteRows(table, criteria)
118
+ return NextResponse.json({ data: rows, deleted: rows.length })
119
+ } catch (error) {
120
+ return toErrorResponse(error, "Failed to delete rows")
121
+ }
122
+ }
123
+
124
+ function parseRowQueryOptions(params: URLSearchParams): RowQueryOptions {
125
+ const limitParam = params.get("limit")
126
+ const offsetParam = params.get("offset")
127
+ const orderBy = params.get("orderBy") ?? undefined
128
+ const orderDirection = parseOrderDirection(params.get("orderDirection"))
129
+ const filters = parseFilters(params)
130
+
131
+ const limit = limitParam ? Number.parseInt(limitParam, 10) : undefined
132
+ const offset = offsetParam ? Number.parseInt(offsetParam, 10) : undefined
133
+
134
+ return {
135
+ limit: Number.isNaN(limit) ? undefined : limit,
136
+ offset: Number.isNaN(offset) ? undefined : offset,
137
+ order_by: orderBy ?? undefined,
138
+ order_direction: orderDirection,
139
+ filters: filters.length ? filters : undefined
140
+ }
141
+ }
142
+
143
+ function parseFilters(params: URLSearchParams): SqliteWhereFilter[] {
144
+ const filters: SqliteWhereFilter[] = []
145
+
146
+ params.forEach((value, key) => {
147
+ const match = key.match(/^filter\[(.+?)\](?:\[(.+)\])?$/)
148
+ if (!match) {
149
+ return
150
+ }
151
+ const column = match[1]
152
+ const operatorValue = (match[2] ?? "eq").toLowerCase()
153
+ if (!isAllowedOperator(operatorValue)) {
154
+ throw new Error(`Unsupported filter operator '${operatorValue}'`)
155
+ }
156
+ filters.push({
157
+ column,
158
+ operator: operatorValue as SqliteFilterOperator,
159
+ value
160
+ })
161
+ })
162
+
163
+ return filters
164
+ }
165
+
166
+ function parseOrderDirection(
167
+ direction: string | null
168
+ ): "asc" | "desc" | undefined {
169
+ if (!direction) {
170
+ return undefined
171
+ }
172
+ const normalized = direction.toLowerCase()
173
+ if (normalized === "asc" || normalized === "desc") {
174
+ return normalized
175
+ }
176
+ throw new Error(`Unsupported order direction '${direction}'`)
177
+ }
178
+
179
+ function isAllowedOperator(value: string): value is SqliteFilterOperator {
180
+ return allowedOperators.includes(value as SqliteFilterOperator)
181
+ }
182
+
183
+ function toErrorResponse(error: unknown, fallback: string) {
184
+ const message = error instanceof Error ? error.message : fallback
185
+ const status = message.toLowerCase().includes("required") ? 400 : 500
186
+ return NextResponse.json({ error: message }, { status })
187
+ }
188
+
@@ -0,0 +1,31 @@
1
+ import { NextRequest, NextResponse } from "next/server"
2
+ import { getSqliteAdminService } from "hazo_connect"
3
+
4
+ export const dynamic = "force-dynamic"
5
+
6
+ export async function GET(request: NextRequest) {
7
+ const service = getSqliteAdminService()
8
+ const url = new URL(request.url)
9
+ const table = url.searchParams.get("table")
10
+
11
+ if (!table) {
12
+ return NextResponse.json(
13
+ { error: "Query parameter 'table' is required." },
14
+ { status: 400 }
15
+ )
16
+ }
17
+
18
+ try {
19
+ const schema = await service.getTableSchema(table)
20
+ return NextResponse.json({ data: schema })
21
+ } catch (error) {
22
+ return toErrorResponse(error, `Failed to load schema for table '${table}'`)
23
+ }
24
+ }
25
+
26
+ function toErrorResponse(error: unknown, fallback: string) {
27
+ const message = error instanceof Error ? error.message : fallback
28
+ const status = message.toLowerCase().includes("required") ? 400 : 500
29
+ return NextResponse.json({ error: message }, { status })
30
+ }
31
+
@@ -0,0 +1,21 @@
1
+ import { NextResponse } from "next/server"
2
+ import { getSqliteAdminService } from "hazo_connect"
3
+
4
+ export const dynamic = "force-dynamic"
5
+
6
+ export async function GET() {
7
+ try {
8
+ const service = getSqliteAdminService()
9
+ const tables = await service.listTables()
10
+ return NextResponse.json({ data: tables })
11
+ } catch (error) {
12
+ return toErrorResponse(error, "Failed to list SQLite tables")
13
+ }
14
+ }
15
+
16
+ function toErrorResponse(error: unknown, fallback: string) {
17
+ const message = error instanceof Error ? error.message : fallback
18
+ const status = message.toLowerCase().includes("required") ? 400 : 500
19
+ return NextResponse.json({ error: message }, { status })
20
+ }
21
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Purpose: Middleware to check if SQLite admin UI is enabled before allowing access to routes.
3
+ *
4
+ * This middleware ensures that the admin UI routes are only accessible when explicitly enabled
5
+ * in the hazo_connect configuration.
6
+ */
7
+
8
+ import { NextResponse } from "next/server"
9
+ import type { NextRequest } from "next/server"
10
+
11
+ export function middleware(request: NextRequest) {
12
+ // Check if this is an admin UI route
13
+ if (
14
+ request.nextUrl.pathname.startsWith("/hazo_connect/sqlite_admin") ||
15
+ request.nextUrl.pathname.startsWith("/hazo_connect/api/sqlite")
16
+ ) {
17
+ // Check if admin UI is enabled via environment variable or config
18
+ // The admin service will throw an error if not enabled, but we can provide a better message here
19
+ const adminUiEnabled =
20
+ process.env.HAZO_CONNECT_ENABLE_ADMIN_UI === "true" ||
21
+ process.env.ENABLE_SQLITE_ADMIN_UI === "true"
22
+
23
+ if (!adminUiEnabled) {
24
+ return NextResponse.json(
25
+ {
26
+ error:
27
+ "SQLite admin UI is not enabled. Set 'enable_admin_ui: true' in your hazo_connect configuration or set HAZO_CONNECT_ENABLE_ADMIN_UI=true environment variable."
28
+ },
29
+ { status: 403 }
30
+ )
31
+ }
32
+ }
33
+
34
+ return NextResponse.next()
35
+ }
36
+
37
+ export const config = {
38
+ matcher: ["/hazo_connect/sqlite_admin/:path*", "/hazo_connect/api/sqlite/:path*"]
39
+ }
40
+
@@ -0,0 +1,26 @@
1
+ import { getSqliteAdminService, type TableSummary } from "hazo_connect"
2
+ import SqliteAdminClient from "./sqlite-admin-client"
3
+
4
+ export const dynamic = "force-dynamic"
5
+
6
+ export default async function SqliteAdminPage() {
7
+ const service = getSqliteAdminService()
8
+
9
+ try {
10
+ const tables = await service.listTables()
11
+ return <SqliteAdminClient initialTables={tables} />
12
+ } catch (error) {
13
+ const message =
14
+ error instanceof Error ? error.message : "Failed to initialise SQLite admin UI."
15
+
16
+ return (
17
+ <section className="mx-auto flex max-w-4xl flex-col gap-4 p-6">
18
+ <h1 className="text-2xl font-semibold text-slate-900">SQLite Admin</h1>
19
+ <p className="rounded-md border border-red-200 bg-red-50 p-4 text-sm text-red-700">
20
+ {message}
21
+ </p>
22
+ </section>
23
+ )
24
+ }
25
+ }
26
+