dzql 0.1.0-alpha.1

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,136 @@
1
+ -- DZQL Framework - Event System and Real-time Notifications
2
+ -- Events are created ONLY by API operations (generic_save, generic_delete)
3
+ -- This ensures proper user context and security
4
+
5
+ -- ============================================================================
6
+ -- NOTIFY EVENT FUNCTION
7
+ -- ============================================================================
8
+ -- Event notification trigger - handles all real-time notifications
9
+ CREATE OR REPLACE FUNCTION dzql.notify_event()
10
+ RETURNS TRIGGER LANGUAGE plpgsql AS $$
11
+ BEGIN
12
+ -- Send real-time notification to single channel
13
+ -- For DELETE operations, send the 'before' data since 'after' is NULL
14
+ PERFORM pg_notify('dzql', jsonb_build_object(
15
+ 'event_id', NEW.event_id,
16
+ 'table', NEW.table_name,
17
+ 'op', NEW.op,
18
+ 'pk', NEW.pk,
19
+ 'data', COALESCE(NEW.after, NEW.before),
20
+ 'before', NEW.before,
21
+ 'after', NEW.after,
22
+ 'user_id', NEW.user_id,
23
+ 'at', NEW.at,
24
+ 'notify_users', NEW.notify_users
25
+ )::text);
26
+
27
+ RETURN NULL;
28
+ END $$;
29
+
30
+ -- ============================================================================
31
+ -- CREATE TRIGGER ON EVENTS TABLE
32
+ -- ============================================================================
33
+ -- Create trigger on events table to handle notifications
34
+ DROP TRIGGER IF EXISTS dzql_events_notify ON dzql.events;
35
+ CREATE TRIGGER dzql_events_notify
36
+ AFTER INSERT ON dzql.events
37
+ FOR EACH ROW EXECUTE FUNCTION dzql.notify_event();
38
+
39
+ -- ============================================================================
40
+ -- HELPER FUNCTIONS
41
+ -- ============================================================================
42
+
43
+ -- Get event history for a specific record (audit trail)
44
+ CREATE OR REPLACE FUNCTION dzql.get_record_history(
45
+ p_table_name text,
46
+ p_record_id text,
47
+ p_limit int DEFAULT 50
48
+ ) RETURNS jsonb
49
+ LANGUAGE sql STABLE AS $$
50
+ SELECT COALESCE(jsonb_agg(
51
+ to_jsonb(e) ORDER BY e.at DESC
52
+ ), '[]'::jsonb)
53
+ FROM (
54
+ SELECT * FROM dzql.events e
55
+ WHERE e.table_name = p_table_name
56
+ AND e.pk->>'id' = p_record_id
57
+ ORDER BY e.at DESC
58
+ LIMIT p_limit
59
+ ) e;
60
+ $$;
61
+
62
+ -- Get recent actions by a user
63
+ CREATE OR REPLACE FUNCTION dzql.get_user_actions(
64
+ p_user_id int,
65
+ p_limit int DEFAULT 100
66
+ ) RETURNS jsonb
67
+ LANGUAGE sql STABLE AS $$
68
+ SELECT COALESCE(jsonb_agg(
69
+ to_jsonb(e) ORDER BY e.at DESC
70
+ ), '[]'::jsonb)
71
+ FROM (
72
+ SELECT * FROM dzql.events e
73
+ WHERE e.user_id = p_user_id
74
+ ORDER BY e.at DESC
75
+ LIMIT p_limit
76
+ ) e;
77
+ $$;
78
+
79
+ -- Get event catchup data for synchronization
80
+ CREATE OR REPLACE FUNCTION dzql.catchup(p_context_id text, p_since_event_id bigint)
81
+ RETURNS jsonb LANGUAGE sql STABLE AS $$
82
+ SELECT coalesce(jsonb_agg(to_jsonb(e) order by e.event_id), '[]'::jsonb)
83
+ FROM dzql.events e
84
+ WHERE e.event_id > p_since_event_id;
85
+ $$;
86
+
87
+ -- Get single event by ID
88
+ CREATE OR REPLACE FUNCTION dzql.get_event(p_event_id bigint)
89
+ RETURNS jsonb LANGUAGE sql STABLE AS $$
90
+ SELECT to_jsonb(e) FROM dzql.events e WHERE e.event_id = p_event_id;
91
+ $$;
92
+
93
+ -- Get recent events on a table
94
+ CREATE OR REPLACE FUNCTION dzql.get_table_events(
95
+ p_table_name text,
96
+ p_limit int DEFAULT 100
97
+ ) RETURNS jsonb
98
+ LANGUAGE sql STABLE AS $$
99
+ SELECT COALESCE(jsonb_agg(
100
+ to_jsonb(e) ORDER BY e.at DESC
101
+ ), '[]'::jsonb)
102
+ FROM (
103
+ SELECT * FROM dzql.events e
104
+ WHERE e.table_name = p_table_name
105
+ ORDER BY e.at DESC
106
+ LIMIT p_limit
107
+ ) e;
108
+ $$;
109
+
110
+ -- Comments
111
+ COMMENT ON FUNCTION dzql.notify_event() IS 'Broadcasts event notifications via PostgreSQL NOTIFY for real-time updates';
112
+ COMMENT ON FUNCTION dzql.get_record_history(text, text, int) IS 'Returns audit trail for a specific record';
113
+ COMMENT ON FUNCTION dzql.get_user_actions(int, int) IS 'Returns recent actions performed by a user';
114
+ COMMENT ON FUNCTION dzql.get_table_events(text, int) IS 'Returns recent events for a table';
115
+
116
+ -- ============================================================================
117
+ -- CLEANUP OLD TRIGGER SYSTEM
118
+ -- ============================================================================
119
+ -- Remove any table triggers that were created by the old system
120
+ DO $$
121
+ DECLARE
122
+ l_table_name text;
123
+ BEGIN
124
+ -- Remove old emit_row_change function if it exists
125
+ DROP FUNCTION IF EXISTS dzql.emit_row_change() CASCADE;
126
+
127
+ -- Clean up any existing table triggers from registered entities
128
+ FOR l_table_name IN
129
+ SELECT table_name FROM dzql.entities
130
+ LOOP
131
+ EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I',
132
+ l_table_name || '_dzql_events', l_table_name);
133
+ END LOOP;
134
+
135
+ RAISE NOTICE 'Cleaned up old table trigger system';
136
+ END $$;
@@ -0,0 +1,18 @@
1
+ -- Hello World Function for Testing DZQL's Function Proxy
2
+
3
+ -- Create a simple hello world function
4
+ -- First parameter must be p_user_id for DZQL compatibility
5
+ create or replace function hello(p_user_id int, p_name text default 'World')
6
+ returns jsonb
7
+ language plpgsql
8
+ security definer
9
+ as $$
10
+ begin
11
+ return jsonb_build_object(
12
+ 'message', 'Hello, ' || coalesce(p_name, 'World') || '!',
13
+ 'timestamp', now(),
14
+ 'from', 'PostgreSQL',
15
+ 'user_id', p_user_id
16
+ );
17
+ end;
18
+ $$;
@@ -0,0 +1,165 @@
1
+ -- === Entity Metadata Function ===
2
+ -- Returns complete metadata for all registered entities in the DZQL system
3
+ -- Includes entity config, schema information, and relationship graph
4
+ -- Used by UI to dynamically configure based on registered entities
5
+
6
+ create or replace function get_entities_metadata(p_user_id int)
7
+ returns jsonb
8
+ language plpgsql
9
+ security definer
10
+ as $$
11
+ declare
12
+ v_entities jsonb;
13
+ v_relations jsonb;
14
+ v_junction_tables text[];
15
+ v_entity_names text[];
16
+ begin
17
+ -- Get list of registered entity names
18
+ select array_agg(table_name) into v_entity_names
19
+ from dzql.entities;
20
+
21
+ -- Build entities object with schema information
22
+ select jsonb_object_agg(
23
+ e.table_name,
24
+ jsonb_build_object(
25
+ 'table_name', e.table_name,
26
+ 'label_field', e.label_field,
27
+ 'searchable_fields', e.searchable_fields,
28
+ 'fk_includes', e.fk_includes,
29
+ 'soft_delete', e.soft_delete,
30
+ 'temporal_fields', e.temporal_fields,
31
+ 'notification_paths', e.notification_paths,
32
+ 'permission_paths', e.permission_paths,
33
+ 'schema', (
34
+ -- Get column schema from information_schema
35
+ select jsonb_agg(
36
+ jsonb_build_object(
37
+ 'column_name', c.column_name,
38
+ 'data_type', c.data_type,
39
+ 'is_nullable', c.is_nullable = 'YES',
40
+ 'column_default', c.column_default,
41
+ 'character_maximum_length', c.character_maximum_length,
42
+ 'numeric_precision', c.numeric_precision,
43
+ 'numeric_scale', c.numeric_scale,
44
+ 'ordinal_position', c.ordinal_position
45
+ ) order by c.ordinal_position
46
+ )
47
+ from information_schema.columns c
48
+ where c.table_schema = 'public'
49
+ and c.table_name = e.table_name
50
+ )
51
+ )
52
+ ) into v_entities
53
+ from dzql.entities e;
54
+
55
+ -- Identify junction tables (2+ foreign keys, minimal searchable fields)
56
+ select array_agg(distinct tc.table_name) into v_junction_tables
57
+ from information_schema.table_constraints tc
58
+ where tc.constraint_type = 'FOREIGN KEY'
59
+ and tc.table_schema = 'public'
60
+ and tc.table_name = any(v_entity_names)
61
+ group by tc.table_name
62
+ having count(*) >= 2
63
+ and (
64
+ select count(*)
65
+ from unnest((
66
+ select searchable_fields
67
+ from dzql.entities
68
+ where table_name = tc.table_name
69
+ )) as sf
70
+ where sf not in (
71
+ select kcu.column_name
72
+ from information_schema.key_column_usage kcu
73
+ where kcu.table_name = tc.table_name
74
+ and kcu.constraint_name in (
75
+ select constraint_name
76
+ from information_schema.table_constraints
77
+ where table_name = tc.table_name
78
+ and constraint_type = 'FOREIGN KEY'
79
+ )
80
+ )
81
+ ) <= 1;
82
+
83
+ -- Build relations array
84
+ with fk_relations as (
85
+ -- Get all foreign key relationships
86
+ select
87
+ tc.table_name,
88
+ kcu.column_name,
89
+ ccu.table_name as foreign_table_name,
90
+ ccu.column_name as foreign_column_name
91
+ from information_schema.table_constraints tc
92
+ join information_schema.key_column_usage kcu
93
+ on tc.constraint_name = kcu.constraint_name
94
+ and tc.table_schema = kcu.table_schema
95
+ join information_schema.constraint_column_usage ccu
96
+ on ccu.constraint_name = tc.constraint_name
97
+ and ccu.table_schema = tc.table_schema
98
+ where tc.constraint_type = 'FOREIGN KEY'
99
+ and tc.table_schema = 'public'
100
+ and tc.table_name = any(v_entity_names)
101
+ and ccu.table_name = any(v_entity_names)
102
+ ),
103
+ simple_relations as (
104
+ -- Many-to-one and one-to-many for non-junction tables
105
+ select jsonb_build_object(
106
+ 'type', 'many_to_one',
107
+ 'from', fk.table_name || '.' || fk.column_name,
108
+ 'to', fk.foreign_table_name || '.' || fk.foreign_column_name
109
+ ) as relation
110
+ from fk_relations fk
111
+ where not (fk.table_name = any(v_junction_tables))
112
+
113
+ union all
114
+
115
+ select jsonb_build_object(
116
+ 'type', 'one_to_many',
117
+ 'from', fk.foreign_table_name || '.' || fk.foreign_column_name,
118
+ 'to', fk.table_name || '.' || fk.column_name
119
+ ) as relation
120
+ from fk_relations fk
121
+ where not (fk.table_name = any(v_junction_tables))
122
+ ),
123
+ junction_relations as (
124
+ -- Many-to-many through junction tables
125
+ select jsonb_build_object(
126
+ 'type', 'many_to_many',
127
+ 'from', fk1.foreign_table_name || '.' || fk1.foreign_column_name,
128
+ 'to', fk2.foreign_table_name || '.' || fk2.foreign_column_name,
129
+ 'via', fk1.table_name || '.' || fk1.column_name || '.' || fk2.column_name
130
+ ) as relation
131
+ from fk_relations fk1
132
+ join fk_relations fk2
133
+ on fk1.table_name = fk2.table_name
134
+ and fk1.column_name < fk2.column_name -- avoid duplicates
135
+ where fk1.table_name = any(v_junction_tables)
136
+
137
+ union all
138
+
139
+ select jsonb_build_object(
140
+ 'type', 'many_to_many',
141
+ 'from', fk2.foreign_table_name || '.' || fk2.foreign_column_name,
142
+ 'to', fk1.foreign_table_name || '.' || fk1.foreign_column_name,
143
+ 'via', fk1.table_name || '.' || fk2.column_name || '.' || fk1.column_name
144
+ ) as relation
145
+ from fk_relations fk1
146
+ join fk_relations fk2
147
+ on fk1.table_name = fk2.table_name
148
+ and fk1.column_name < fk2.column_name
149
+ where fk1.table_name = any(v_junction_tables)
150
+ )
151
+ select jsonb_agg(relation) into v_relations
152
+ from (
153
+ select relation from simple_relations
154
+ union all
155
+ select relation from junction_relations
156
+ ) all_relations;
157
+
158
+ -- Return complete metadata structure
159
+ return jsonb_build_object(
160
+ 'entities', v_entities,
161
+ 'relations', coalesce(v_relations, '[]'::jsonb),
162
+ 'operations', jsonb_build_array('get', 'save', 'delete', 'lookup', 'search')
163
+ );
164
+ end;
165
+ $$;
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ // ZeroQL Framework - Main Entry Point
2
+ export { createServer } from './server/index.js';
3
+
4
+ // Re-export client utilities
5
+ export { WebSocketManager, useWs } from './client/ws.js';
6
+
7
+ // Re-export UI framework
8
+ export { mount, state, Component } from './client/ui.js';
9
+ export { loadUI, loadEntityUI } from './client/ui-loader.js';
10
+
11
+ // Re-export database utilities for tests and custom functions
12
+ export { sql, listen_sql, db } from './server/db.js';
13
+ export { createWebSocketHandlers, verify_jwt_token } from './server/ws.js';
14
+
15
+ // Re-export meta route for applications
16
+ export { metaRoute } from './server/meta-route.js';
17
+
18
+ // Re-export MCP route for Claude Code integration
19
+ export { createMCPRoute } from './server/mcp.js';
@@ -0,0 +1,9 @@
1
+ export async function goodbye(userId, params = {}) {
2
+ const { name = "World" } = params;
3
+
4
+ return {
5
+ message: `Goodbye, ${name}!`,
6
+ from: "Bun",
7
+ user_id: userId,
8
+ };
9
+ }
@@ -0,0 +1,261 @@
1
+ import postgres from "postgres";
2
+ import { dbLogger, notifyLogger } from "./logger.js";
3
+
4
+ // Environment configuration
5
+ const DATABASE_URL =
6
+ process.env.DATABASE_URL ||
7
+ "postgresql://dzql:dzql@localhost:5432/dzql";
8
+
9
+ const DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS || "10", 10);
10
+ const DB_IDLE_TIMEOUT = parseInt(process.env.DB_IDLE_TIMEOUT || "20", 10);
11
+ const DB_CONNECT_TIMEOUT = parseInt(process.env.DB_CONNECT_TIMEOUT || "10", 10);
12
+
13
+ // Main PostgreSQL connection for queries
14
+ export const sql = postgres(DATABASE_URL, {
15
+ max: DB_MAX_CONNECTIONS,
16
+ idle_timeout: DB_IDLE_TIMEOUT,
17
+ connect_timeout: DB_CONNECT_TIMEOUT,
18
+ // Suppress NOTICE messages in test environment
19
+ onnotice: process.env.NODE_ENV === 'test' ? () => {} : undefined,
20
+ });
21
+
22
+ // Separate PostgreSQL connection for NOTIFY/LISTEN
23
+ export const listen_sql = postgres(DATABASE_URL, {
24
+ max: 1,
25
+ idle_timeout: 0,
26
+ connect_timeout: DB_CONNECT_TIMEOUT,
27
+ // Suppress NOTICE messages in test environment
28
+ onnotice: process.env.NODE_ENV === 'test' ? () => {} : undefined,
29
+ });
30
+
31
+ dbLogger.info(`Database connected: ${DATABASE_URL.replace(/\/\/.*@/, '//***@')}`);
32
+
33
+ // Cache for function parameter metadata
34
+ const functionParamCache = new Map();
35
+
36
+ // Cache helpers
37
+ export async function getCache(key, ttlHours) {
38
+ const result = await sql`SELECT app._get_cache(${key}, ${ttlHours}) as data`;
39
+ return result[0]?.data ? JSON.parse(result[0].data) : null;
40
+ }
41
+
42
+ export async function setCache(key, data) {
43
+ await sql`SELECT app._set_cache(${key}, ${JSON.stringify(data)})`;
44
+ }
45
+
46
+ // Auth helpers
47
+ export async function callAuthFunction(method, email, password) {
48
+ const result = await sql`
49
+ SELECT ${sql(method)}(${email}, ${password}) as result
50
+ `;
51
+ return result[0].result;
52
+ }
53
+
54
+ // Get function parameter metadata
55
+ async function getFunctionParams(functionName) {
56
+ if (functionParamCache.has(functionName)) {
57
+ return functionParamCache.get(functionName);
58
+ }
59
+
60
+ const result = await sql`
61
+ SELECT
62
+ p.parameter_name,
63
+ p.parameter_default,
64
+ p.data_type,
65
+ p.ordinal_position
66
+ FROM information_schema.parameters p
67
+ WHERE p.specific_name IN (
68
+ SELECT r.specific_name
69
+ FROM information_schema.routines r
70
+ WHERE r.routine_name = ${functionName}
71
+ AND r.routine_type = 'FUNCTION'
72
+ )
73
+ AND (p.parameter_mode = 'IN' OR p.parameter_mode IS NULL)
74
+ ORDER BY p.ordinal_position
75
+ `;
76
+
77
+ const params = result.map((row) => ({
78
+ name: row.parameter_name,
79
+ type: row.data_type,
80
+ position: row.ordinal_position,
81
+ hasDefault: row.parameter_default !== null,
82
+ }));
83
+
84
+ functionParamCache.set(functionName, params);
85
+ return params;
86
+ }
87
+
88
+ // Generic stored function call with user_id
89
+ export async function callUserFunction(method, userId, params) {
90
+ // Validate function name format (only alphanumeric and underscore, no special chars)
91
+ // This prevents SQL injection via function names like "foo(); DROP TABLE users--"
92
+ if (!/^[a-z_][a-z0-9_]*$/i.test(method)) {
93
+ throw new Error(`Invalid function name: ${method}`);
94
+ }
95
+
96
+ const functionParams = await getFunctionParams(method);
97
+
98
+ if (functionParams.length === 0) {
99
+ throw new Error(`Function ${method} not found`);
100
+ }
101
+
102
+ // Build ordered parameter array
103
+ const orderedParams = [];
104
+
105
+ for (const param of functionParams) {
106
+ if (param.position === 1) {
107
+ // First parameter is always user_id
108
+ orderedParams.push(userId);
109
+ } else {
110
+ // Strip p_ prefix from parameter name for client API matching
111
+ const clientParamName = param.name.startsWith("p_")
112
+ ? param.name.substring(2)
113
+ : param.name;
114
+
115
+ if (params && params[clientParamName] !== undefined) {
116
+ // Parameter exists in the params object
117
+ orderedParams.push(params[clientParamName]);
118
+ } else if (param.hasDefault) {
119
+ // Parameter has a default value, skip it
120
+ break;
121
+ } else {
122
+ // Required parameter missing
123
+ throw new Error(`Missing required parameter: ${clientParamName}`);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Try table format first - works for both single and multiple results
129
+ const query = `SELECT * FROM ${method}(${orderedParams.map((_, i) => `$${i + 1}`).join(", ")})`;
130
+ const result = await sql.unsafe(query, orderedParams);
131
+
132
+ // If single row with single column, return just the value
133
+ if (result.length === 1 && Object.keys(result[0]).length === 1) {
134
+ return Object.values(result[0])[0];
135
+ }
136
+
137
+ // Otherwise return the full result set
138
+ return result;
139
+ }
140
+
141
+ // Get user profile
142
+ export async function getUserProfile(userId) {
143
+ const result = await sql`
144
+ SELECT _profile(${userId}::integer) as profile
145
+ `;
146
+ return result[0].profile;
147
+ }
148
+
149
+ // Setup NOTIFY listeners
150
+ export async function setupListeners(callback) {
151
+ try {
152
+ // Listen to single dzql channel for all events
153
+ await listen_sql.listen("dzql", (payload) => {
154
+ const event = JSON.parse(payload);
155
+ notifyLogger.debug(`Received NOTIFY event:`, event.table, event.op);
156
+ callback(event);
157
+ });
158
+ notifyLogger.info("NOTIFY listener established on 'dzql' channel");
159
+ return true;
160
+ } catch (error) {
161
+ notifyLogger.error("Failed to setup listeners:", error.message);
162
+ return false;
163
+ }
164
+ }
165
+
166
+ // DZQL Generic Operations
167
+ export async function callDZQLOperation(operation, entity, args, userId) {
168
+ dbLogger.trace(`DZQL ${operation}.${entity} for user ${userId}`);
169
+ const result = await sql`
170
+ SELECT dzql.generic_exec(${operation}, ${entity}, ${args}, ${userId}) as result
171
+ `;
172
+ return result[0].result;
173
+ }
174
+
175
+ // DZQL nested proxy factory
176
+ function createEntityProxy(operation) {
177
+ return new Proxy(
178
+ {},
179
+ {
180
+ get(target, entityName) {
181
+ return async (args = {}, userId) => {
182
+ // userId is required for DZQL operations
183
+ if (!userId) {
184
+ throw new Error("userId is required for DZQL operations");
185
+ }
186
+ return callDZQLOperation(operation, entityName, args, userId);
187
+ };
188
+ },
189
+ },
190
+ );
191
+ }
192
+
193
+ // DZQL database API proxy with custom function support
194
+ export const db = {
195
+ api: new Proxy(
196
+ {
197
+ get: createEntityProxy("get"),
198
+ save: createEntityProxy("save"),
199
+ delete: createEntityProxy("delete"),
200
+ lookup: createEntityProxy("lookup"),
201
+ search: createEntityProxy("search"),
202
+ exec: async (functionName, args, userId) => {
203
+ if (!userId) {
204
+ throw new Error("userId is required for function calls");
205
+ }
206
+ return callUserFunction(functionName, userId, args);
207
+ },
208
+ // Permission and path resolution utilities
209
+ checkPermission: async (userId, operation, entity, record) => {
210
+ const result = await sql`
211
+ SELECT dzql.check_permission(${userId}, ${operation}, ${entity}, ${JSON.stringify(record)}) as allowed
212
+ `;
213
+ return result[0].allowed;
214
+ },
215
+ resolveNotificationPath: async (tableName, record, path) => {
216
+ const result = await sql`
217
+ SELECT dzql.resolve_notification_path(${tableName}, ${JSON.stringify(record)}, ${path}) as user_ids
218
+ `;
219
+ return result[0].user_ids;
220
+ },
221
+ resolveNotificationPaths: async (tableName, record) => {
222
+ const result = await sql`
223
+ SELECT dzql.resolve_notification_paths(${tableName}, ${JSON.stringify(record)}) as user_ids
224
+ `;
225
+ return result[0].user_ids;
226
+ },
227
+ },
228
+ {
229
+ get(target, prop) {
230
+ // Return existing DZQL operations
231
+ if (target[prop]) {
232
+ return target[prop];
233
+ }
234
+
235
+ // Handle custom functions
236
+ return async (userIdOrArgs, args = {}) => {
237
+ // Special handling for auth functions that don't require userId
238
+ if (prop === 'register_user' || prop === 'login_user') {
239
+ // For auth functions, first param is the args object
240
+ return callAuthFunction(prop, userIdOrArgs.email, userIdOrArgs.password);
241
+ }
242
+
243
+ // For other functions, userId is required as first parameter
244
+ if (!userIdOrArgs) {
245
+ throw new Error(`userId is required for function ${prop}`);
246
+ }
247
+
248
+ return callUserFunction(prop, userIdOrArgs, args);
249
+ };
250
+ },
251
+ }
252
+ ),
253
+ };
254
+
255
+ // Graceful shutdown
256
+ export async function closeConnections() {
257
+ dbLogger.info("Closing database connections...");
258
+ await sql.end();
259
+ await listen_sql.end();
260
+ dbLogger.info("Database connections closed");
261
+ }