dzql 0.5.23 → 0.5.24
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
|
@@ -11,6 +11,18 @@ export class OperationCodegen {
|
|
|
11
11
|
this.isCompositePK = this.primaryKey.length > 1 || this.primaryKey[0] !== 'id';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Determine appropriate type cast for a column name
|
|
16
|
+
* Only casts to int if column is 'id' or ends with '_id'
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
_getTypeCast(columnName) {
|
|
20
|
+
if (columnName === 'id' || columnName.endsWith('_id')) {
|
|
21
|
+
return '::int';
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/**
|
|
15
27
|
* Generate all operation functions
|
|
16
28
|
* @returns {string} SQL for all operations
|
|
@@ -36,7 +48,7 @@ export class OperationCodegen {
|
|
|
36
48
|
// For composite PKs, accept JSONB containing all PK fields
|
|
37
49
|
// For simple PKs, accept INT for backwards compatibility
|
|
38
50
|
if (this.isCompositePK) {
|
|
39
|
-
const whereClause = this.primaryKey.map(col => `${col} = (p_pk->>'${col}')
|
|
51
|
+
const whereClause = this.primaryKey.map(col => `${col} = (p_pk->>'${col}')${this._getTypeCast(col)}`).join(' AND ');
|
|
40
52
|
const pkDescription = this.primaryKey.join(', ');
|
|
41
53
|
|
|
42
54
|
return `-- GET operation for ${this.tableName} (composite primary key: ${pkDescription})
|
|
@@ -125,6 +137,21 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
125
137
|
const m2mExpansion = this._generateM2MExpansion();
|
|
126
138
|
const fieldDefaults = this._generateFieldDefaults();
|
|
127
139
|
|
|
140
|
+
// For composite PKs, generate a different function signature and logic
|
|
141
|
+
if (this.isCompositePK) {
|
|
142
|
+
return this._generateCompositePKSaveFunction({
|
|
143
|
+
graphRulesCall,
|
|
144
|
+
notificationSQL,
|
|
145
|
+
filterSensitiveFields,
|
|
146
|
+
m2mVariables,
|
|
147
|
+
m2mExtraction,
|
|
148
|
+
m2mSync,
|
|
149
|
+
m2mExpansion,
|
|
150
|
+
fieldDefaults
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Simple PK (id column) - original implementation for backwards compatibility
|
|
128
155
|
return `-- SAVE operation for ${this.tableName}
|
|
129
156
|
CREATE OR REPLACE FUNCTION save_${this.tableName}(
|
|
130
157
|
p_user_id INT,
|
|
@@ -207,6 +234,141 @@ ${m2mExpansion}
|
|
|
207
234
|
${graphRulesCall}
|
|
208
235
|
${notificationSQL}
|
|
209
236
|
|
|
237
|
+
${filterSensitiveFields}
|
|
238
|
+
|
|
239
|
+
RETURN v_output;
|
|
240
|
+
END;
|
|
241
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate SAVE function for composite primary keys
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_generateCompositePKSaveFunction(helpers) {
|
|
249
|
+
const {
|
|
250
|
+
graphRulesCall,
|
|
251
|
+
notificationSQL,
|
|
252
|
+
filterSensitiveFields,
|
|
253
|
+
m2mVariables,
|
|
254
|
+
m2mExtraction,
|
|
255
|
+
m2mSync,
|
|
256
|
+
m2mExpansion,
|
|
257
|
+
fieldDefaults
|
|
258
|
+
} = helpers;
|
|
259
|
+
|
|
260
|
+
const pkDescription = this.primaryKey.join(', ');
|
|
261
|
+
|
|
262
|
+
// Generate WHERE clause for composite PK lookup
|
|
263
|
+
// e.g., "entity_type = (p_data->>'entity_type') AND entity_id = (p_data->>'entity_id')::int"
|
|
264
|
+
const whereClause = this.primaryKey.map(col => {
|
|
265
|
+
return `${col} = (p_data->>'${col}')${this._getTypeCast(col)}`;
|
|
266
|
+
}).join(' AND ');
|
|
267
|
+
|
|
268
|
+
// For use inside format() string literal with %L placeholders
|
|
269
|
+
// e.g., "entity_type = %L AND entity_id = %L"
|
|
270
|
+
const whereClauseForFormat = this.primaryKey.map(col => `${col} = %L`).join(' AND ');
|
|
271
|
+
|
|
272
|
+
// Format arguments for the WHERE clause (to be passed to format())
|
|
273
|
+
// e.g., "(p_data->>'entity_type'), (p_data->>'entity_id')::int"
|
|
274
|
+
const whereClauseFormatArgs = this.primaryKey.map(col => {
|
|
275
|
+
return `(p_data->>'${col}')${this._getTypeCast(col)}`;
|
|
276
|
+
}).join(', ');
|
|
277
|
+
|
|
278
|
+
// Check if all PK fields are present in p_data
|
|
279
|
+
const pkNullCheck = this.primaryKey.map(col => `p_data->>'${col}' IS NULL`).join(' OR ');
|
|
280
|
+
|
|
281
|
+
// Build the list of PK field names for exclusion from SET clause
|
|
282
|
+
const pkFieldsExclusion = this.primaryKey.map(col => `jsonb_object_keys != '${col}'`).join(' AND ');
|
|
283
|
+
|
|
284
|
+
// For M2M sync and expansion, we need to reference the PK fields from v_result
|
|
285
|
+
const m2mSyncCompositePK = this._generateM2MSyncCompositePK();
|
|
286
|
+
const m2mExpansionCompositePK = this._generateM2MExpansionCompositePK();
|
|
287
|
+
const m2mExpansionForBeforeCompositePK = this._generateM2MExpansionForBeforeCompositePK();
|
|
288
|
+
|
|
289
|
+
return `-- SAVE operation for ${this.tableName} (composite primary key: ${pkDescription})
|
|
290
|
+
CREATE OR REPLACE FUNCTION save_${this.tableName}(
|
|
291
|
+
p_user_id INT,
|
|
292
|
+
p_data JSONB
|
|
293
|
+
) RETURNS JSONB AS $$
|
|
294
|
+
DECLARE
|
|
295
|
+
v_result ${this.tableName}%ROWTYPE;
|
|
296
|
+
v_existing ${this.tableName}%ROWTYPE;
|
|
297
|
+
v_output JSONB;
|
|
298
|
+
v_before JSONB;
|
|
299
|
+
v_is_insert BOOLEAN := false;
|
|
300
|
+
v_notify_users INT[];
|
|
301
|
+
${m2mVariables}
|
|
302
|
+
BEGIN
|
|
303
|
+
${m2mExtraction}
|
|
304
|
+
-- Determine if this is insert or update (composite PK: ${pkDescription})
|
|
305
|
+
IF ${pkNullCheck} THEN
|
|
306
|
+
-- Missing one or more PK fields - this is an insert
|
|
307
|
+
v_is_insert := true;
|
|
308
|
+
ELSE
|
|
309
|
+
-- Try to fetch existing record by composite PK
|
|
310
|
+
SELECT * INTO v_existing
|
|
311
|
+
FROM ${this.tableName}
|
|
312
|
+
WHERE ${whereClause};
|
|
313
|
+
|
|
314
|
+
v_is_insert := NOT FOUND;
|
|
315
|
+
END IF;
|
|
316
|
+
|
|
317
|
+
-- Check permissions
|
|
318
|
+
IF v_is_insert THEN
|
|
319
|
+
IF NOT can_create_${this.tableName}(p_user_id, p_data) THEN
|
|
320
|
+
RAISE EXCEPTION 'Permission denied: create on ${this.tableName}';
|
|
321
|
+
END IF;
|
|
322
|
+
ELSE
|
|
323
|
+
IF NOT can_update_${this.tableName}(p_user_id, to_jsonb(v_existing)) THEN
|
|
324
|
+
RAISE EXCEPTION 'Permission denied: update on ${this.tableName}';
|
|
325
|
+
END IF;
|
|
326
|
+
END IF;
|
|
327
|
+
|
|
328
|
+
-- Expand M2M for existing record (for UPDATE events "before" field)
|
|
329
|
+
IF NOT v_is_insert THEN
|
|
330
|
+
v_before := to_jsonb(v_existing);
|
|
331
|
+
${m2mExpansionForBeforeCompositePK}
|
|
332
|
+
END IF;
|
|
333
|
+
${fieldDefaults}
|
|
334
|
+
-- Perform UPSERT
|
|
335
|
+
IF v_is_insert THEN
|
|
336
|
+
-- Dynamic INSERT from JSONB
|
|
337
|
+
EXECUTE (
|
|
338
|
+
SELECT format(
|
|
339
|
+
'INSERT INTO ${this.tableName} (%s) VALUES (%s) RETURNING *',
|
|
340
|
+
string_agg(quote_ident(key), ', '),
|
|
341
|
+
string_agg(quote_nullable(value), ', ')
|
|
342
|
+
)
|
|
343
|
+
FROM jsonb_each_text(p_data) kv(key, value)
|
|
344
|
+
) INTO v_result;
|
|
345
|
+
ELSE
|
|
346
|
+
-- Dynamic UPDATE from JSONB (only if there are non-PK fields to update)
|
|
347
|
+
IF (SELECT COUNT(*) FROM jsonb_object_keys(p_data) WHERE ${pkFieldsExclusion}) > 0 THEN
|
|
348
|
+
EXECUTE (
|
|
349
|
+
SELECT format(
|
|
350
|
+
'UPDATE ${this.tableName} SET %s WHERE ${whereClauseForFormat} RETURNING *',
|
|
351
|
+
string_agg(quote_ident(key) || ' = ' || quote_nullable(value), ', '),
|
|
352
|
+
${whereClauseFormatArgs}
|
|
353
|
+
)
|
|
354
|
+
FROM jsonb_each_text(p_data) kv(key, value)
|
|
355
|
+
WHERE ${this.primaryKey.map(col => `key != '${col}'`).join(' AND ')}
|
|
356
|
+
) INTO v_result;
|
|
357
|
+
ELSE
|
|
358
|
+
-- No fields to update (only M2M fields were provided), just fetch existing
|
|
359
|
+
v_result := v_existing;
|
|
360
|
+
END IF;
|
|
361
|
+
END IF;
|
|
362
|
+
|
|
363
|
+
${m2mSyncCompositePK}
|
|
364
|
+
|
|
365
|
+
-- Prepare output with M2M fields (BEFORE event creation for real-time notifications!)
|
|
366
|
+
v_output := to_jsonb(v_result);
|
|
367
|
+
${m2mExpansionCompositePK}
|
|
368
|
+
|
|
369
|
+
${graphRulesCall}
|
|
370
|
+
${notificationSQL}
|
|
371
|
+
|
|
210
372
|
${filterSensitiveFields}
|
|
211
373
|
|
|
212
374
|
RETURN v_output;
|
|
@@ -224,7 +386,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
224
386
|
|
|
225
387
|
// For composite PKs, accept JSONB containing all PK fields
|
|
226
388
|
if (this.isCompositePK) {
|
|
227
|
-
const whereClause = this.primaryKey.map(col => `${col} = (p_pk->>'${col}')
|
|
389
|
+
const whereClause = this.primaryKey.map(col => `${col} = (p_pk->>'${col}')${this._getTypeCast(col)}`).join(' AND ');
|
|
228
390
|
const pkDescription = this.primaryKey.join(', ');
|
|
229
391
|
|
|
230
392
|
const deleteSQL = this.entity.softDelete
|
|
@@ -740,6 +902,129 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
740
902
|
return expansions.join('');
|
|
741
903
|
}
|
|
742
904
|
|
|
905
|
+
/**
|
|
906
|
+
* Generate M2M sync for composite primary keys
|
|
907
|
+
* For tables with composite PKs, M2M relationships are rare but possible
|
|
908
|
+
* @private
|
|
909
|
+
*/
|
|
910
|
+
_generateM2MSyncCompositePK() {
|
|
911
|
+
const manyToMany = this.entity.manyToMany || {};
|
|
912
|
+
if (Object.keys(manyToMany).length === 0) return '';
|
|
913
|
+
|
|
914
|
+
// For composite PKs, we need to build a composite key reference
|
|
915
|
+
// This is a rare case - most tables with composite PKs are junction tables themselves
|
|
916
|
+
const syncs = [];
|
|
917
|
+
|
|
918
|
+
for (const [relationKey, config] of Object.entries(manyToMany)) {
|
|
919
|
+
const idField = config.id_field;
|
|
920
|
+
const junctionTable = config.junction_table;
|
|
921
|
+
const localKey = config.local_key;
|
|
922
|
+
const foreignKey = config.foreign_key;
|
|
923
|
+
|
|
924
|
+
// For composite PK tables with M2M, we'd need a different junction table structure
|
|
925
|
+
// This is an edge case - log a warning comment in the generated SQL
|
|
926
|
+
syncs.push(`
|
|
927
|
+
-- ============================================================================
|
|
928
|
+
-- M2M Sync: ${relationKey} (junction: ${junctionTable}) - COMPOSITE PK
|
|
929
|
+
-- Note: M2M on composite PK tables requires junction table to reference all PK columns
|
|
930
|
+
-- ============================================================================
|
|
931
|
+
IF v_${idField} IS NOT NULL THEN
|
|
932
|
+
-- Delete relationships not in new list (using first PK column as reference)
|
|
933
|
+
DELETE FROM ${junctionTable}
|
|
934
|
+
WHERE ${localKey} = v_result.${this.primaryKey[0]}
|
|
935
|
+
AND (${foreignKey} <> ALL(v_${idField}) OR v_${idField} = '{}');
|
|
936
|
+
|
|
937
|
+
-- Insert new relationships (idempotent)
|
|
938
|
+
IF array_length(v_${idField}, 1) > 0 THEN
|
|
939
|
+
INSERT INTO ${junctionTable} (${localKey}, ${foreignKey})
|
|
940
|
+
SELECT v_result.${this.primaryKey[0]}, unnest(v_${idField})
|
|
941
|
+
ON CONFLICT (${localKey}, ${foreignKey}) DO NOTHING;
|
|
942
|
+
END IF;
|
|
943
|
+
END IF;`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return syncs.join('');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Generate M2M expansion for composite primary keys (for SAVE output)
|
|
951
|
+
* @private
|
|
952
|
+
*/
|
|
953
|
+
_generateM2MExpansionCompositePK() {
|
|
954
|
+
const manyToMany = this.entity.manyToMany || {};
|
|
955
|
+
if (Object.keys(manyToMany).length === 0) return '';
|
|
956
|
+
|
|
957
|
+
const expansions = [];
|
|
958
|
+
|
|
959
|
+
for (const [relationKey, config] of Object.entries(manyToMany)) {
|
|
960
|
+
const idField = config.id_field;
|
|
961
|
+
const junctionTable = config.junction_table;
|
|
962
|
+
const localKey = config.local_key;
|
|
963
|
+
const foreignKey = config.foreign_key;
|
|
964
|
+
const targetEntity = config.target_entity;
|
|
965
|
+
const expand = config.expand || false;
|
|
966
|
+
|
|
967
|
+
// Use first PK column for M2M lookups
|
|
968
|
+
expansions.push(`
|
|
969
|
+
-- Add M2M IDs: ${idField} (composite PK table)
|
|
970
|
+
v_output := v_output || jsonb_build_object('${idField}',
|
|
971
|
+
(SELECT COALESCE(jsonb_agg(${foreignKey} ORDER BY ${foreignKey}), '[]'::jsonb)
|
|
972
|
+
FROM ${junctionTable} WHERE ${localKey} = v_result.${this.primaryKey[0]})
|
|
973
|
+
);`);
|
|
974
|
+
|
|
975
|
+
if (expand) {
|
|
976
|
+
expansions.push(`
|
|
977
|
+
-- Expand M2M objects: ${relationKey} (expand=true, composite PK table)
|
|
978
|
+
v_output := v_output || jsonb_build_object('${relationKey}',
|
|
979
|
+
(SELECT COALESCE(jsonb_agg(to_jsonb(t.*) ORDER BY t.id), '[]'::jsonb)
|
|
980
|
+
FROM ${junctionTable} jt
|
|
981
|
+
JOIN ${targetEntity} t ON t.id = jt.${foreignKey}
|
|
982
|
+
WHERE jt.${localKey} = v_result.${this.primaryKey[0]})
|
|
983
|
+
);`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return expansions.join('');
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Generate M2M expansion for "before" record with composite primary keys
|
|
992
|
+
* @private
|
|
993
|
+
*/
|
|
994
|
+
_generateM2MExpansionForBeforeCompositePK() {
|
|
995
|
+
const manyToMany = this.entity.manyToMany || {};
|
|
996
|
+
if (Object.keys(manyToMany).length === 0) return '';
|
|
997
|
+
|
|
998
|
+
const expansions = [];
|
|
999
|
+
|
|
1000
|
+
for (const [relationKey, config] of Object.entries(manyToMany)) {
|
|
1001
|
+
const idField = config.id_field;
|
|
1002
|
+
const junctionTable = config.junction_table;
|
|
1003
|
+
const localKey = config.local_key;
|
|
1004
|
+
const foreignKey = config.foreign_key;
|
|
1005
|
+
const targetEntity = config.target_entity;
|
|
1006
|
+
const expand = config.expand || false;
|
|
1007
|
+
|
|
1008
|
+
expansions.push(`
|
|
1009
|
+
v_before := v_before || jsonb_build_object('${idField}',
|
|
1010
|
+
(SELECT COALESCE(jsonb_agg(${foreignKey} ORDER BY ${foreignKey}), '[]'::jsonb)
|
|
1011
|
+
FROM ${junctionTable} WHERE ${localKey} = v_existing.${this.primaryKey[0]})
|
|
1012
|
+
);`);
|
|
1013
|
+
|
|
1014
|
+
if (expand) {
|
|
1015
|
+
expansions.push(`
|
|
1016
|
+
v_before := v_before || jsonb_build_object('${relationKey}',
|
|
1017
|
+
(SELECT COALESCE(jsonb_agg(to_jsonb(t.*) ORDER BY t.id), '[]'::jsonb)
|
|
1018
|
+
FROM ${junctionTable} jt
|
|
1019
|
+
JOIN ${targetEntity} t ON t.id = jt.${foreignKey}
|
|
1020
|
+
WHERE jt.${localKey} = v_existing.${this.primaryKey[0]})
|
|
1021
|
+
);`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return expansions.join('');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
743
1028
|
/**
|
|
744
1029
|
* Generate FK expansions for GET
|
|
745
1030
|
* @private
|
package/src/server/namespace.js
CHANGED
|
@@ -10,6 +10,7 @@ const DEFAULT_USER_ID = 1;
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Discover available entities from dzql.entities table or compiled functions
|
|
13
|
+
* @returns {Promise<Object>} Map of entity name to {label, searchable, description}
|
|
13
14
|
*/
|
|
14
15
|
async function discoverEntities() {
|
|
15
16
|
// First try dzql.entities table (runtime mode)
|
|
@@ -58,6 +59,9 @@ async function discoverEntities() {
|
|
|
58
59
|
/**
|
|
59
60
|
* DZQL operations namespace - provides CLI-style access to DZQL operations
|
|
60
61
|
*
|
|
62
|
+
* Each method outputs JSON to console and calls sql.end() before returning,
|
|
63
|
+
* making instances single-use. On error, methods call process.exit(1).
|
|
64
|
+
*
|
|
61
65
|
* Usage in tasks.js:
|
|
62
66
|
* ```js
|
|
63
67
|
* import { DzqlNamespace } from 'dzql/namespace';
|
|
@@ -70,11 +74,17 @@ async function discoverEntities() {
|
|
|
70
74
|
* ```
|
|
71
75
|
*/
|
|
72
76
|
export class DzqlNamespace {
|
|
77
|
+
/**
|
|
78
|
+
* @param {number} [userId=1] - User ID for permission checks
|
|
79
|
+
*/
|
|
73
80
|
constructor(userId = DEFAULT_USER_ID) {
|
|
74
81
|
this.userId = userId;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* List all available entities
|
|
86
|
+
* @returns {Promise<void>} Outputs JSON to console
|
|
87
|
+
*/
|
|
78
88
|
async entities(c) {
|
|
79
89
|
try {
|
|
80
90
|
const entities = await discoverEntities();
|
|
@@ -89,7 +99,12 @@ export class DzqlNamespace {
|
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Search an entity
|
|
104
|
+
* @example invj dzql:search venues '{"query": "test"}'
|
|
105
|
+
* @param {string} entity - Entity/table name to search
|
|
106
|
+
* @param {string} [argsJson] - JSON string with search args (query, limit, offset, filters)
|
|
107
|
+
*/
|
|
93
108
|
async search(c, entity, argsJson = "{}") {
|
|
94
109
|
if (!entity) {
|
|
95
110
|
console.error("Error: entity name required");
|
|
@@ -123,7 +138,12 @@ export class DzqlNamespace {
|
|
|
123
138
|
}
|
|
124
139
|
}
|
|
125
140
|
|
|
126
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Get entity by ID
|
|
143
|
+
* @example invj dzql:get venues '{"id": 1}'
|
|
144
|
+
* @param {string} entity - Entity/table name
|
|
145
|
+
* @param {string} [argsJson] - JSON string with {id: number}
|
|
146
|
+
*/
|
|
127
147
|
async get(c, entity, argsJson = "{}") {
|
|
128
148
|
if (!entity) {
|
|
129
149
|
console.error("Error: entity name required");
|
|
@@ -155,7 +175,12 @@ export class DzqlNamespace {
|
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
177
|
|
|
158
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Save (create or update) entity
|
|
180
|
+
* @example invj dzql:save venues '{"name": "New Venue", "org_id": 1}'
|
|
181
|
+
* @param {string} entity - Entity/table name
|
|
182
|
+
* @param {string} [argsJson] - JSON string with entity data (include id to update, omit to create)
|
|
183
|
+
*/
|
|
159
184
|
async save(c, entity, argsJson = "{}") {
|
|
160
185
|
if (!entity) {
|
|
161
186
|
console.error("Error: entity name required");
|
|
@@ -189,7 +214,12 @@ export class DzqlNamespace {
|
|
|
189
214
|
}
|
|
190
215
|
}
|
|
191
216
|
|
|
192
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Delete entity by ID
|
|
219
|
+
* @example invj dzql:delete venues '{"id": 1}'
|
|
220
|
+
* @param {string} entity - Entity/table name
|
|
221
|
+
* @param {string} [argsJson] - JSON string with {id: number}
|
|
222
|
+
*/
|
|
193
223
|
async delete(c, entity, argsJson = "{}") {
|
|
194
224
|
if (!entity) {
|
|
195
225
|
console.error("Error: entity name required");
|
|
@@ -221,7 +251,12 @@ export class DzqlNamespace {
|
|
|
221
251
|
}
|
|
222
252
|
}
|
|
223
253
|
|
|
224
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Lookup entity (for dropdowns/autocomplete)
|
|
256
|
+
* @example invj dzql:lookup organisations '{"query": "acme"}'
|
|
257
|
+
* @param {string} entity - Entity/table name
|
|
258
|
+
* @param {string} [argsJson] - JSON string with {query: string, limit?: number}
|
|
259
|
+
*/
|
|
225
260
|
async lookup(c, entity, argsJson = "{}") {
|
|
226
261
|
if (!entity) {
|
|
227
262
|
console.error("Error: entity name required");
|