dzql 0.3.7 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
4
4
  "description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
5
5
  "type": "module",
6
6
  "main": "src/server/index.js",
@@ -11,7 +11,8 @@
11
11
  "./client/templates": "./src/client/templates/App.vue",
12
12
  "./server": "./src/server/index.js",
13
13
  "./db": "./src/server/db.js",
14
- "./compiler": "./src/compiler/index.js"
14
+ "./compiler": "./src/compiler/index.js",
15
+ "./namespace": "./src/server/namespace.js"
15
16
  },
16
17
  "files": [
17
18
  "bin/**/*.js",
package/src/server/db.js CHANGED
@@ -35,7 +35,10 @@ export const listen_sql = postgres(DATABASE_URL, {
35
35
  onnotice: process.env.NODE_ENV === 'test' ? () => {} : undefined,
36
36
  });
37
37
 
38
- dbLogger.info(`Database connected: ${DATABASE_URL.replace(/\/\/.*@/, '//***@')}`);
38
+ // Only log connection info in development
39
+ if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
40
+ dbLogger.info(`Database connected: ${DATABASE_URL.replace(/\/\/.*@/, '//***@')}`);
41
+ }
39
42
 
40
43
  // Cache for function parameter metadata
41
44
  const functionParamCache = new Map();
@@ -170,13 +173,93 @@ export async function setupListeners(callback) {
170
173
  }
171
174
  }
172
175
 
176
+ // Cache for mode detection (null = not checked, true = compiled, false = runtime)
177
+ let isCompiledMode = null;
178
+
179
+ // Auto-detect if we're in compiled or runtime mode
180
+ async function detectMode() {
181
+ if (isCompiledMode !== null) {
182
+ return isCompiledMode;
183
+ }
184
+
185
+ try {
186
+ // Check if dzql.generic_exec exists
187
+ const result = await sql`
188
+ SELECT 1 FROM pg_proc
189
+ WHERE proname = 'generic_exec'
190
+ AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'dzql')
191
+ LIMIT 1
192
+ `;
193
+ isCompiledMode = result.length === 0; // If no results, it's compiled mode
194
+ dbLogger.trace(isCompiledMode ? 'Detected compiled mode' : 'Detected runtime mode');
195
+ } catch (error) {
196
+ // If there's an error checking, assume runtime mode
197
+ isCompiledMode = false;
198
+ dbLogger.trace('Error detecting mode, assuming runtime mode');
199
+ }
200
+
201
+ return isCompiledMode;
202
+ }
203
+
173
204
  // DZQL Generic Operations
174
205
  export async function callDZQLOperation(operation, entity, args, userId) {
175
206
  dbLogger.trace(`DZQL ${operation}.${entity} for user ${userId}`);
176
- const result = await sql`
177
- SELECT dzql.generic_exec(${operation}, ${entity}, ${args}, ${userId}) as result
178
- `;
179
- return result[0].result;
207
+
208
+ const compiled = await detectMode();
209
+
210
+ if (!compiled) {
211
+ // Runtime mode - use generic_exec
212
+ const result = await sql`
213
+ SELECT dzql.generic_exec(${operation}, ${entity}, ${args}, ${userId}) as result
214
+ `;
215
+ return result[0].result;
216
+ } else {
217
+ // Compiled mode - call compiled function directly
218
+ const compiledFunctionName = `${operation}_${entity}`;
219
+
220
+ // Different operations have different signatures:
221
+ // - search: search_entity(p_user_id, p_filters, p_search, p_sort, p_page, p_limit)
222
+ // - get: get_entity(p_user_id, p_id, p_on_date)
223
+ // - save: save_entity(p_user_id, p_data, p_on_date)
224
+ // - delete: delete_entity(p_user_id, p_id)
225
+ // - lookup: lookup_entity(p_user_id, p_term, p_limit)
226
+
227
+ if (operation === 'search') {
228
+ // Extract search parameters from args
229
+ const filters = args.filters || args.p_filters || {};
230
+ const search = args.search || null;
231
+ const sort = args.sort || null;
232
+ const page = args.page || 1;
233
+ const limit = args.limit || 25;
234
+
235
+ const result = await sql.unsafe(`
236
+ SELECT ${compiledFunctionName}($1::int, $2::jsonb, $3::text, $4::jsonb, $5::int, $6::int) as result
237
+ `, [userId, filters, search, sort, page, limit]);
238
+ return result[0].result;
239
+ } else if (operation === 'get') {
240
+ const result = await sql.unsafe(`
241
+ SELECT ${compiledFunctionName}($1::int, $2::int, NULL) as result
242
+ `, [userId, args.id]);
243
+ return result[0].result;
244
+ } else if (operation === 'save') {
245
+ const result = await sql.unsafe(`
246
+ SELECT ${compiledFunctionName}($1::int, $2::jsonb, NULL) as result
247
+ `, [userId, args]);
248
+ return result[0].result;
249
+ } else if (operation === 'delete') {
250
+ const result = await sql.unsafe(`
251
+ SELECT ${compiledFunctionName}($1::int, $2::int) as result
252
+ `, [userId, args.id]);
253
+ return result[0].result;
254
+ } else if (operation === 'lookup') {
255
+ const result = await sql.unsafe(`
256
+ SELECT ${compiledFunctionName}($1::int, $2::text, $3::int) as result
257
+ `, [userId, args.term || '', args.limit || 10]);
258
+ return result[0].result;
259
+ } else {
260
+ throw new Error(`Unknown operation: ${operation}`);
261
+ }
262
+ }
180
263
  }
181
264
 
182
265
  // DZQL nested proxy factory
@@ -12,6 +12,15 @@ const LOG_LEVELS = {
12
12
  // Default log level from environment or INFO
13
13
  const DEFAULT_LEVEL = process.env.LOG_LEVEL?.toUpperCase() || "INFO";
14
14
 
15
+ // Detect if running in CLI context (invokej/tasks.js)
16
+ const isCliContext = () => {
17
+ // Check if main module contains 'tasks.js' or 'invokej'
18
+ const mainModule = process.argv[1] || '';
19
+ return mainModule.includes('tasks.js') ||
20
+ mainModule.includes('invokej') ||
21
+ mainModule.includes('invj');
22
+ };
23
+
15
24
  // Parse LOG_CATEGORIES from environment
16
25
  // Format: "ws:debug,db:trace,auth:info" or "*:debug" for all
17
26
  const parseCategories = () => {
@@ -20,8 +29,9 @@ const parseCategories = () => {
20
29
 
21
30
  if (!envCategories) {
22
31
  // Default settings for development vs production
23
- if (process.env.NODE_ENV === "production") {
24
- categories["*"] = LOG_LEVELS.WARN;
32
+ // CLI context defaults to ERROR level unless explicitly configured
33
+ if (process.env.NODE_ENV === "production" || isCliContext()) {
34
+ categories["*"] = LOG_LEVELS.ERROR; // Only errors in production/CLI
25
35
  } else if (process.env.NODE_ENV === "test") {
26
36
  categories["*"] = LOG_LEVELS.ERROR;
27
37
  } else {
@@ -220,8 +230,11 @@ export const timed = async (category, operation, fn) => {
220
230
  }
221
231
  };
222
232
 
223
- // Log configuration on startup (only in development)
224
- if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test") {
233
+ // Log configuration on startup (only in development and when explicitly debugging)
234
+ // Suppress banner if LOG_CATEGORIES is not set (user doesn't care about logging config)
235
+ if (process.env.NODE_ENV !== "production" &&
236
+ process.env.NODE_ENV !== "test" &&
237
+ process.env.LOG_CATEGORIES) {
225
238
  const config = getConfig();
226
239
  console.log(colors.bright + "=== Logger Configuration ===" + colors.reset);
227
240
  console.log("Categories:", config.categories);
@@ -0,0 +1,233 @@
1
+ // Suppress logger output for CLI usage - MUST be set before any imports
2
+ if (!process.env.NODE_ENV) {
3
+ process.env.NODE_ENV = 'production';
4
+ }
5
+
6
+ import { sql, db } from "./db.js";
7
+
8
+ // Default user for CLI operations
9
+ const DEFAULT_USER_ID = 1;
10
+
11
+ /**
12
+ * Discover available entities from dzql.entities table
13
+ */
14
+ async function discoverEntities() {
15
+ const result = await sql`
16
+ SELECT table_name, label_field, searchable_fields
17
+ FROM dzql.entities
18
+ ORDER BY table_name
19
+ `;
20
+
21
+ const entities = {};
22
+ for (const row of result) {
23
+ const searchFields = row.searchable_fields?.join(", ") || "none";
24
+ entities[row.table_name] = {
25
+ label: row.label_field,
26
+ searchable: row.searchable_fields || [],
27
+ description: `Entity: ${row.table_name} (label: ${row.label_field}, searchable: ${searchFields})`,
28
+ };
29
+ }
30
+
31
+ return entities;
32
+ }
33
+
34
+ /**
35
+ * DZQL operations namespace - provides CLI-style access to DZQL operations
36
+ *
37
+ * Usage in tasks.js:
38
+ * ```js
39
+ * import { DzqlNamespace } from 'dzql/namespace';
40
+ *
41
+ * export class Tasks {
42
+ * constructor() {
43
+ * this.dzql = new DzqlNamespace();
44
+ * }
45
+ * }
46
+ * ```
47
+ */
48
+ export class DzqlNamespace {
49
+ constructor(userId = DEFAULT_USER_ID) {
50
+ this.userId = userId;
51
+ }
52
+
53
+ /** List all available entities */
54
+ async entities(c) {
55
+ try {
56
+ const entities = await discoverEntities();
57
+ console.log(JSON.stringify({ success: true, entities }, null, 2));
58
+ await sql.end();
59
+ } catch (error) {
60
+ console.error(
61
+ JSON.stringify({ success: false, error: error.message }, null, 2),
62
+ );
63
+ await sql.end();
64
+ process.exit(1);
65
+ }
66
+ }
67
+
68
+ /** Search an entity */
69
+ async search(c, entity, argsJson = "{}") {
70
+ if (!entity) {
71
+ console.error("Error: entity name required");
72
+ console.error("Usage: invokej dzql.search <entity> '<json_args>'");
73
+ console.error(
74
+ 'Example: invokej dzql.search organisations \'{"query": "test"}\'',
75
+ );
76
+ await sql.end();
77
+ process.exit(1);
78
+ }
79
+
80
+ let args;
81
+ try {
82
+ args = JSON.parse(argsJson);
83
+ } catch (e) {
84
+ console.error("Error: arguments must be valid JSON");
85
+ await sql.end();
86
+ process.exit(1);
87
+ }
88
+
89
+ try {
90
+ const result = await db.api.search[entity](args, this.userId);
91
+ console.log(JSON.stringify({ success: true, result }, null, 2));
92
+ await sql.end();
93
+ } catch (error) {
94
+ console.error(
95
+ JSON.stringify({ success: false, error: error.message }, null, 2),
96
+ );
97
+ await sql.end();
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ /** Get entity by ID */
103
+ async get(c, entity, argsJson = "{}") {
104
+ if (!entity) {
105
+ console.error("Error: entity name required");
106
+ console.error("Usage: invokej dzql.get <entity> '<json_args>'");
107
+ console.error("Example: invokej dzql.get venues '{\"id\": 1}'");
108
+ await sql.end();
109
+ process.exit(1);
110
+ }
111
+
112
+ let args;
113
+ try {
114
+ args = JSON.parse(argsJson);
115
+ } catch (e) {
116
+ console.error("Error: arguments must be valid JSON");
117
+ await sql.end();
118
+ process.exit(1);
119
+ }
120
+
121
+ try {
122
+ const result = await db.api.get[entity](args, this.userId);
123
+ console.log(JSON.stringify({ success: true, result }, null, 2));
124
+ await sql.end();
125
+ } catch (error) {
126
+ console.error(
127
+ JSON.stringify({ success: false, error: error.message }, null, 2),
128
+ );
129
+ await sql.end();
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ /** Save (create or update) entity */
135
+ async save(c, entity, argsJson = "{}") {
136
+ if (!entity) {
137
+ console.error("Error: entity name required");
138
+ console.error("Usage: invokej dzql.save <entity> '<json_args>'");
139
+ console.error(
140
+ 'Example: invokej dzql.save venues \'{"name": "Test Venue", "org_id": 1}\'',
141
+ );
142
+ await sql.end();
143
+ process.exit(1);
144
+ }
145
+
146
+ let args;
147
+ try {
148
+ args = JSON.parse(argsJson);
149
+ } catch (e) {
150
+ console.error("Error: arguments must be valid JSON");
151
+ await sql.end();
152
+ process.exit(1);
153
+ }
154
+
155
+ try {
156
+ const result = await db.api.save[entity](args, this.userId);
157
+ console.log(JSON.stringify({ success: true, result }, null, 2));
158
+ await sql.end();
159
+ } catch (error) {
160
+ console.error(
161
+ JSON.stringify({ success: false, error: error.message }, null, 2),
162
+ );
163
+ await sql.end();
164
+ process.exit(1);
165
+ }
166
+ }
167
+
168
+ /** Delete entity by ID */
169
+ async delete(c, entity, argsJson = "{}") {
170
+ if (!entity) {
171
+ console.error("Error: entity name required");
172
+ console.error("Usage: invokej dzql.delete <entity> '<json_args>'");
173
+ console.error("Example: invokej dzql.delete venues '{\"id\": 1}'");
174
+ await sql.end();
175
+ process.exit(1);
176
+ }
177
+
178
+ let args;
179
+ try {
180
+ args = JSON.parse(argsJson);
181
+ } catch (e) {
182
+ console.error("Error: arguments must be valid JSON");
183
+ await sql.end();
184
+ process.exit(1);
185
+ }
186
+
187
+ try {
188
+ const result = await db.api.delete[entity](args, this.userId);
189
+ console.log(JSON.stringify({ success: true, result }, null, 2));
190
+ await sql.end();
191
+ } catch (error) {
192
+ console.error(
193
+ JSON.stringify({ success: false, error: error.message }, null, 2),
194
+ );
195
+ await sql.end();
196
+ process.exit(1);
197
+ }
198
+ }
199
+
200
+ /** Lookup entity (for dropdowns/autocomplete) */
201
+ async lookup(c, entity, argsJson = "{}") {
202
+ if (!entity) {
203
+ console.error("Error: entity name required");
204
+ console.error("Usage: invokej dzql.lookup <entity> '<json_args>'");
205
+ console.error(
206
+ 'Example: invokej dzql.lookup organisations \'{"query": "acme"}\'',
207
+ );
208
+ await sql.end();
209
+ process.exit(1);
210
+ }
211
+
212
+ let args;
213
+ try {
214
+ args = JSON.parse(argsJson);
215
+ } catch (e) {
216
+ console.error("Error: arguments must be valid JSON");
217
+ await sql.end();
218
+ process.exit(1);
219
+ }
220
+
221
+ try {
222
+ const result = await db.api.lookup[entity](args, this.userId);
223
+ console.log(JSON.stringify({ success: true, result }, null, 2));
224
+ await sql.end();
225
+ } catch (error) {
226
+ console.error(
227
+ JSON.stringify({ success: false, error: error.message }, null, 2),
228
+ );
229
+ await sql.end();
230
+ process.exit(1);
231
+ }
232
+ }
233
+ }