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 +3 -2
- package/src/server/db.js +88 -5
- package/src/server/logger.js +17 -4
- package/src/server/namespace.js +233 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
package/src/server/logger.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
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 (
|
|
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
|
+
}
|