@dboio/cli 0.6.14 → 0.7.2
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/README.md +185 -0
- package/package.json +1 -1
- package/src/commands/clone.js +650 -25
- package/src/commands/content.js +1 -1
- package/src/commands/deploy.js +1 -1
- package/src/commands/init.js +20 -1
- package/src/commands/login.js +4 -9
- package/src/commands/output.js +56 -3
- package/src/commands/pull.js +3 -3
- package/src/commands/push.js +9 -9
- package/src/lib/config.js +74 -8
- package/src/lib/diff.js +5 -1
- package/src/lib/input-parser.js +87 -38
- package/src/lib/structure.js +16 -0
package/src/commands/content.js
CHANGED
|
@@ -116,7 +116,7 @@ const deployCmd = new Command('deploy')
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
119
|
-
const errorResult = await checkSubmitErrors(result);
|
|
119
|
+
const errorResult = await checkSubmitErrors(result, { rowUid: uid });
|
|
120
120
|
if (errorResult) {
|
|
121
121
|
if (errorResult.skipRecord || errorResult.skipAll) {
|
|
122
122
|
log.info('Submission cancelled');
|
package/src/commands/deploy.js
CHANGED
|
@@ -156,7 +156,7 @@ export const deployCommand = new Command('deploy')
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
159
|
-
const errorResult = await checkSubmitErrors(result);
|
|
159
|
+
const errorResult = await checkSubmitErrors(result, { rowUid: uid });
|
|
160
160
|
if (errorResult) {
|
|
161
161
|
if (errorResult.skipRecord) {
|
|
162
162
|
log.warn(` Skipping "${entryName}"`);
|
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { access, readdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset } from '../lib/config.js';
|
|
4
|
+
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput } from '../lib/config.js';
|
|
5
5
|
import { installOrUpdateClaudeCommands } from './install.js';
|
|
6
6
|
import { scaffoldProjectDirs, logScaffoldResult } from '../lib/scaffold.js';
|
|
7
7
|
import { log } from '../lib/logger.js';
|
|
@@ -107,6 +107,25 @@ export const initCommand = new Command('init')
|
|
|
107
107
|
await saveTransactionKeyPreset('RowUID');
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// Prompt for TicketSuggestionOutput
|
|
111
|
+
const DEFAULT_TICKET_SUGGESTION_OUTPUT = 'ojaie9t3o0kfvliahnuuda';
|
|
112
|
+
if (!options.nonInteractive) {
|
|
113
|
+
const inquirer = (await import('inquirer')).default;
|
|
114
|
+
const { ticketSuggestionOutput } = await inquirer.prompt([{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'ticketSuggestionOutput',
|
|
117
|
+
message: 'Output UID for ticket suggestions (leave blank to skip):',
|
|
118
|
+
default: DEFAULT_TICKET_SUGGESTION_OUTPUT,
|
|
119
|
+
}]);
|
|
120
|
+
const tsVal = ticketSuggestionOutput.trim();
|
|
121
|
+
if (tsVal) {
|
|
122
|
+
await saveTicketSuggestionOutput(tsVal);
|
|
123
|
+
log.dim(` TicketSuggestionOutput: ${tsVal}`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
await saveTicketSuggestionOutput(DEFAULT_TICKET_SUGGESTION_OUTPUT);
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
// ─── Scaffold ─────────────────────────────────────────────────────────────
|
|
111
130
|
let shouldScaffold = options.scaffold;
|
|
112
131
|
|
package/src/commands/login.js
CHANGED
|
@@ -50,22 +50,17 @@ export const loginCommand = new Command('login')
|
|
|
50
50
|
// Ensure sensitive files are gitignored
|
|
51
51
|
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/ticketing.local.json']);
|
|
52
52
|
|
|
53
|
-
// Fetch current user info to store ID
|
|
53
|
+
// Fetch current user info to store ID for future submissions
|
|
54
54
|
try {
|
|
55
55
|
const userResult = await client.get('/api/output/31799e38f0854956a47f10', { '_format': 'json_raw' });
|
|
56
56
|
const userData = userResult.payload || userResult.data;
|
|
57
57
|
const rows = Array.isArray(userData) ? userData : (userData?.Rows || userData?.rows || []);
|
|
58
58
|
if (rows.length > 0) {
|
|
59
59
|
const row = rows[0];
|
|
60
|
-
const userUid = row.UID || row.uid;
|
|
61
60
|
const userId = row.ID || row.id || row.UserID || row.userId;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (info.userUid || info.userId) {
|
|
66
|
-
await saveUserInfo(info);
|
|
67
|
-
if (info.userId) log.dim(` User ID: ${info.userId}`);
|
|
68
|
-
if (info.userUid) log.dim(` User UID: ${info.userUid}`);
|
|
61
|
+
if (userId) {
|
|
62
|
+
await saveUserInfo({ userId: String(userId) });
|
|
63
|
+
log.dim(` User ID: ${userId}`);
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
// Save user profile (name, email) for package.json population
|
package/src/commands/output.js
CHANGED
|
@@ -25,6 +25,22 @@ export const outputCommand = new Command('output')
|
|
|
25
25
|
.option('--template <value>', 'Custom template')
|
|
26
26
|
.option('--debug', 'Include debug info')
|
|
27
27
|
.option('--debug-sql', 'Include SQL debug info')
|
|
28
|
+
.option('--debug-verbose', 'Verbose debug output')
|
|
29
|
+
.option('--debug-analysis', 'Analysis debug output')
|
|
30
|
+
.option('--limit <n>', 'Maximum rows to return (_limit)')
|
|
31
|
+
.option('--rowcount <bool>', 'Include row count: true (default) or false for performance')
|
|
32
|
+
.option('--display <expr>', 'Show/hide template tags (repeatable), e.g., "sidebar=hide"', collect, [])
|
|
33
|
+
.option('--format-values', 'Enable value formatting in json_raw output')
|
|
34
|
+
.option('--empty-response-code <code>', 'HTTP status code when output returns no results')
|
|
35
|
+
.option('--fallback-content <expr>', 'Fallback content UID for error codes, e.g., "404=contentUID"')
|
|
36
|
+
.option('--escape-html <bool>', 'Control HTML escaping: true or false')
|
|
37
|
+
.option('--mime <type>', 'Override MIME/content type')
|
|
38
|
+
.option('--strict', 'Strict error mode')
|
|
39
|
+
.option('--confirm', 'Confirmation flag')
|
|
40
|
+
.option('--include <expr>', 'Include token')
|
|
41
|
+
.option('--no-transaction', 'Disable transaction wrapping')
|
|
42
|
+
.option('--skip <phase>', 'Skip execution phases (repeatable, admin-only)', collect, [])
|
|
43
|
+
.option('--profile', 'Enable MiniProfiler output')
|
|
28
44
|
.option('--meta', 'Use meta output endpoint')
|
|
29
45
|
.option('--meta-column <uid>', 'Column metadata')
|
|
30
46
|
.option('--save', 'Interactive save-to-disk mode')
|
|
@@ -66,8 +82,45 @@ export const outputCommand = new Command('output')
|
|
|
66
82
|
if (options.maxrows) params['_maxrows'] = options.maxrows;
|
|
67
83
|
if (options.rows) params['_rows'] = options.rows;
|
|
68
84
|
if (options.template) params['_template'] = options.template;
|
|
85
|
+
if (options.limit) params['_limit'] = options.limit;
|
|
86
|
+
if (options.rowcount !== undefined) params['_rowcount'] = options.rowcount;
|
|
69
87
|
if (options.debug) params['_debug'] = 'true';
|
|
70
88
|
if (options.debugSql) params['_debug_sql'] = 'true';
|
|
89
|
+
if (options.debugVerbose) params['_debug:verbose'] = 'true';
|
|
90
|
+
if (options.debugAnalysis) params['_debug:analysis'] = 'true';
|
|
91
|
+
if (options.formatValues) params['_format_values'] = 'true';
|
|
92
|
+
if (options.emptyResponseCode) params['_empty_response_code'] = options.emptyResponseCode;
|
|
93
|
+
if (options.escapeHtml !== undefined) params['_escape_html'] = options.escapeHtml;
|
|
94
|
+
if (options.mime) params['_mime'] = options.mime;
|
|
95
|
+
if (options.strict) params['_strict'] = 'true';
|
|
96
|
+
if (options.confirm) params['_confirm'] = 'true';
|
|
97
|
+
if (options.include) params['_include'] = options.include;
|
|
98
|
+
if (options.transaction === false) params['_no_transaction'] = 'true';
|
|
99
|
+
if (options.profile) params['_profile'] = 'true';
|
|
100
|
+
|
|
101
|
+
// --display: "tagName=show|hide" → _display@tagName=show|hide
|
|
102
|
+
for (const d of options.display) {
|
|
103
|
+
const eqIdx = d.indexOf('=');
|
|
104
|
+
if (eqIdx !== -1) {
|
|
105
|
+
params[`_display@${d.substring(0, eqIdx)}`] = d.substring(eqIdx + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --fallback-content: "404=contentUID" → _fallback_content:404=contentUID
|
|
110
|
+
if (options.fallbackContent) {
|
|
111
|
+
const fb = options.fallbackContent;
|
|
112
|
+
if (fb.includes('=')) {
|
|
113
|
+
const [code, uid] = fb.split('=', 2);
|
|
114
|
+
params[`_fallback_content:${code}`] = uid;
|
|
115
|
+
} else {
|
|
116
|
+
params['_fallback_content'] = fb;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --skip: repeatable → array
|
|
121
|
+
if (options.skip.length > 0) {
|
|
122
|
+
params['_skip'] = options.skip;
|
|
123
|
+
}
|
|
71
124
|
|
|
72
125
|
for (const f of options.filter) {
|
|
73
126
|
const atIdx = f.indexOf('=');
|
|
@@ -84,9 +137,9 @@ export const outputCommand = new Command('output')
|
|
|
84
137
|
}
|
|
85
138
|
}
|
|
86
139
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
params[
|
|
140
|
+
// Pass sort values through directly — API expects _sort=Column:DIR
|
|
141
|
+
if (options.sort.length > 0) {
|
|
142
|
+
params['_sort'] = options.sort;
|
|
90
143
|
}
|
|
91
144
|
|
|
92
145
|
// For save mode or jq mode, force json_raw if user didn't set a custom format
|
package/src/commands/pull.js
CHANGED
|
@@ -40,9 +40,9 @@ export const pullCommand = new Command('pull')
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
params[
|
|
43
|
+
// Pass sort values through directly — API expects _sort=Column:DIR
|
|
44
|
+
if (options.sort.length > 0) {
|
|
45
|
+
params['_sort'] = options.sort;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
let path;
|
package/src/commands/push.js
CHANGED
|
@@ -13,7 +13,7 @@ import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
|
13
13
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
14
14
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
15
15
|
import { detectChangedColumns, findBaselineEntry } from '../lib/delta.js';
|
|
16
|
-
import {
|
|
16
|
+
import { ENTITY_DEPENDENCIES } from '../lib/dependencies.js';
|
|
17
17
|
|
|
18
18
|
export const pushCommand = new Command('push')
|
|
19
19
|
.description('Push local files back to DBO.io using metadata from pull')
|
|
@@ -262,13 +262,12 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
265
|
+
// Sort by dependency level: children first (ascending level) for add/edit operations
|
|
266
|
+
toPush.sort((a, b) => {
|
|
267
|
+
const levelA = ENTITY_DEPENDENCIES[a.meta._entity] || 0;
|
|
268
|
+
const levelB = ENTITY_DEPENDENCIES[b.meta._entity] || 0;
|
|
269
|
+
return levelA - levelB;
|
|
270
|
+
});
|
|
272
271
|
|
|
273
272
|
// Process in dependency order
|
|
274
273
|
let succeeded = 0;
|
|
@@ -361,6 +360,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
361
360
|
for (const [key, value] of Object.entries(meta)) {
|
|
362
361
|
if (shouldSkipColumn(key)) continue;
|
|
363
362
|
if (key === 'UID') continue; // UID is the identifier, not a column to update
|
|
363
|
+
if (key === 'children') continue; // Output hierarchy structural field, not a server column
|
|
364
364
|
if (value === null || value === undefined) continue;
|
|
365
365
|
|
|
366
366
|
// Delta sync: skip columns not in changedColumns
|
|
@@ -413,7 +413,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
416
|
-
const retryResult = await checkSubmitErrors(result);
|
|
416
|
+
const retryResult = await checkSubmitErrors(result, { rowUid: uid });
|
|
417
417
|
if (retryResult) {
|
|
418
418
|
// Handle skip actions
|
|
419
419
|
if (retryResult.skipRecord) {
|
package/src/lib/config.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function initConfig(domain) {
|
|
|
69
69
|
|
|
70
70
|
export async function saveCredentials(username) {
|
|
71
71
|
await mkdir(dboDir(), { recursive: true });
|
|
72
|
-
// Preserve existing fields (like
|
|
72
|
+
// Preserve existing fields (like userId) — never store password
|
|
73
73
|
let existing = {};
|
|
74
74
|
try {
|
|
75
75
|
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
@@ -79,14 +79,13 @@ export async function saveCredentials(username) {
|
|
|
79
79
|
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export async function saveUserInfo({ userId
|
|
82
|
+
export async function saveUserInfo({ userId }) {
|
|
83
83
|
await mkdir(dboDir(), { recursive: true });
|
|
84
84
|
let existing = {};
|
|
85
85
|
try {
|
|
86
86
|
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
87
87
|
} catch { /* no existing file */ }
|
|
88
88
|
if (userId !== undefined) existing.userId = userId;
|
|
89
|
-
if (userUid !== undefined) existing.userUid = userUid;
|
|
90
89
|
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -94,12 +93,9 @@ export async function loadUserInfo() {
|
|
|
94
93
|
try {
|
|
95
94
|
const raw = await readFile(credentialsPath(), 'utf8');
|
|
96
95
|
const creds = JSON.parse(raw);
|
|
97
|
-
return {
|
|
98
|
-
userId: creds.userId || null,
|
|
99
|
-
userUid: creds.userUid || null,
|
|
100
|
-
};
|
|
96
|
+
return { userId: creds.userId || null };
|
|
101
97
|
} catch {
|
|
102
|
-
return { userId: null
|
|
98
|
+
return { userId: null };
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
101
|
|
|
@@ -529,6 +525,50 @@ export async function getAllPluginScopes() {
|
|
|
529
525
|
return result;
|
|
530
526
|
}
|
|
531
527
|
|
|
528
|
+
// ─── Output Hierarchy Filename Preferences ────────────────────────────────
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Config keys for output hierarchy filename column preferences.
|
|
532
|
+
* Maps physical table names to config key names.
|
|
533
|
+
*/
|
|
534
|
+
const OUTPUT_FILENAME_CONFIG_KEYS = {
|
|
535
|
+
output: 'OutputFilenameCol',
|
|
536
|
+
output_value: 'OutputColumnFilenameCol',
|
|
537
|
+
output_value_filter: 'OutputFilterFilenameCol',
|
|
538
|
+
output_value_entity_column_rel: 'OutputJoinFilenameCol',
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Load output filename column preference for an entity type.
|
|
543
|
+
* @param {string} entityKey - Physical table name (e.g., 'output', 'output_value')
|
|
544
|
+
* @returns {Promise<string|null>}
|
|
545
|
+
*/
|
|
546
|
+
export async function loadOutputFilenamePreference(entityKey) {
|
|
547
|
+
try {
|
|
548
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
549
|
+
const config = JSON.parse(raw);
|
|
550
|
+
const configKey = OUTPUT_FILENAME_CONFIG_KEYS[entityKey];
|
|
551
|
+
return configKey ? (config[configKey] || null) : null;
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Save output filename column preference for an entity type.
|
|
559
|
+
* @param {string} entityKey - Physical table name
|
|
560
|
+
* @param {string} columnName - Column name to use as filename
|
|
561
|
+
*/
|
|
562
|
+
export async function saveOutputFilenamePreference(entityKey, columnName) {
|
|
563
|
+
const configKey = OUTPUT_FILENAME_CONFIG_KEYS[entityKey];
|
|
564
|
+
if (!configKey) return;
|
|
565
|
+
await mkdir(dboDir(), { recursive: true });
|
|
566
|
+
let existing = {};
|
|
567
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
568
|
+
existing[configKey] = columnName;
|
|
569
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
570
|
+
}
|
|
571
|
+
|
|
532
572
|
// ─── AppModifyKey ─────────────────────────────────────────────────────────
|
|
533
573
|
|
|
534
574
|
/**
|
|
@@ -580,6 +620,32 @@ export async function loadTransactionKeyPreset() {
|
|
|
580
620
|
} catch { return null; }
|
|
581
621
|
}
|
|
582
622
|
|
|
623
|
+
// ─── TicketSuggestionOutput ────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Save TicketSuggestionOutput to .dbo/config.json.
|
|
627
|
+
* This is the output UID used to fetch ticket suggestions.
|
|
628
|
+
*/
|
|
629
|
+
export async function saveTicketSuggestionOutput(outputUid) {
|
|
630
|
+
await mkdir(dboDir(), { recursive: true });
|
|
631
|
+
let existing = {};
|
|
632
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
633
|
+
if (outputUid != null) existing.TicketSuggestionOutput = outputUid;
|
|
634
|
+
else delete existing.TicketSuggestionOutput;
|
|
635
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Load TicketSuggestionOutput from .dbo/config.json.
|
|
640
|
+
* Returns the output UID string or null if not set.
|
|
641
|
+
*/
|
|
642
|
+
export async function loadTicketSuggestionOutput() {
|
|
643
|
+
try {
|
|
644
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
645
|
+
return JSON.parse(raw).TicketSuggestionOutput || null;
|
|
646
|
+
} catch { return null; }
|
|
647
|
+
}
|
|
648
|
+
|
|
583
649
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|
|
584
650
|
|
|
585
651
|
/**
|
package/src/lib/diff.js
CHANGED
|
@@ -28,7 +28,8 @@ async function fileExists(path) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Recursively find all
|
|
31
|
+
* Recursively find all metadata files in a directory.
|
|
32
|
+
* Includes .metadata.json files and output hierarchy files (_output~*.json).
|
|
32
33
|
*/
|
|
33
34
|
export async function findMetadataFiles(dir) {
|
|
34
35
|
const results = [];
|
|
@@ -42,6 +43,9 @@ export async function findMetadataFiles(dir) {
|
|
|
42
43
|
results.push(...await findMetadataFiles(fullPath));
|
|
43
44
|
} else if (entry.name.endsWith('.metadata.json')) {
|
|
44
45
|
results.push(fullPath);
|
|
46
|
+
} else if (entry.name.startsWith('_output~') && entry.name.endsWith('.json') && !entry.name.includes('.CustomSQL.')) {
|
|
47
|
+
// Output hierarchy files: _output~<name>~<uid>.json and nested entity files
|
|
48
|
+
results.push(fullPath);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
package/src/lib/input-parser.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { log } from './logger.js';
|
|
3
|
-
import { loadUserInfo } from './config.js';
|
|
3
|
+
import { loadUserInfo, loadTicketSuggestionOutput } from './config.js';
|
|
4
4
|
import { buildTicketExpression, setGlobalTicket, setRecordTicket, getGlobalTicket, getRecordTicket } from './ticketing.js';
|
|
5
|
+
import { DboClient } from './client.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Parse DBO input syntax and build form data.
|
|
@@ -84,6 +85,7 @@ function findValueAtSign(expr) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// Patterns that indicate a missing user identity in the server response
|
|
88
|
+
// Note: the user entity only has an ID (never a UID), so all patterns resolve to _OverrideUserID.
|
|
87
89
|
const USER_ID_PATTERNS = [
|
|
88
90
|
'LoggedInUser_UID',
|
|
89
91
|
'LoggedInUserID',
|
|
@@ -101,7 +103,7 @@ const USER_ID_PATTERNS = [
|
|
|
101
103
|
* - repo_mismatch → repository mismatch recovery (6 options)
|
|
102
104
|
* - ticket_lookup_required_error → prompts for Ticket ID (legacy)
|
|
103
105
|
* - LoggedInUser_UID, LoggedInUserID, CurrentUserID, UserID not found
|
|
104
|
-
* → prompts for User ID
|
|
106
|
+
* → prompts for User ID (session not authenticated)
|
|
105
107
|
*
|
|
106
108
|
* Returns an object with retry information, or null if no recoverable errors found.
|
|
107
109
|
*
|
|
@@ -109,9 +111,9 @@ const USER_ID_PATTERNS = [
|
|
|
109
111
|
* { retryParams, ticketExpressions, skipRecord, skipAll }
|
|
110
112
|
*
|
|
111
113
|
* Return shape for legacy/user errors (backward compatible):
|
|
112
|
-
* { _OverrideTicketID,
|
|
114
|
+
* { _OverrideTicketID, _OverrideUserID, ... }
|
|
113
115
|
*/
|
|
114
|
-
export async function checkSubmitErrors(result) {
|
|
116
|
+
export async function checkSubmitErrors(result, context = {}) {
|
|
115
117
|
const messages = result.messages || result.data?.Messages || [];
|
|
116
118
|
const allText = messages.filter(m => typeof m === 'string').join(' ');
|
|
117
119
|
|
|
@@ -121,7 +123,7 @@ export async function checkSubmitErrors(result) {
|
|
|
121
123
|
const hasRepoMismatch = allText.includes('repo_mismatch');
|
|
122
124
|
|
|
123
125
|
if (hasTicketError) {
|
|
124
|
-
return await handleTicketError(allText);
|
|
126
|
+
return await handleTicketError(allText, context);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
if (hasRepoMismatch) {
|
|
@@ -132,7 +134,6 @@ export async function checkSubmitErrors(result) {
|
|
|
132
134
|
const needsTicket = allText.includes('ticket_lookup_required_error');
|
|
133
135
|
const matchedUserPattern = USER_ID_PATTERNS.find(p => allText.includes(p));
|
|
134
136
|
const needsUser = !!matchedUserPattern;
|
|
135
|
-
const needsUserUid = matchedUserPattern && matchedUserPattern.includes('UID');
|
|
136
137
|
|
|
137
138
|
if (!needsTicket && !needsUser) return null;
|
|
138
139
|
|
|
@@ -142,14 +143,9 @@ export async function checkSubmitErrors(result) {
|
|
|
142
143
|
if (!process.stdin.isTTY) {
|
|
143
144
|
if (needsUser) {
|
|
144
145
|
const stored = await loadUserInfo();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (needsUserUid) {
|
|
149
|
-
retryParams['_OverrideUserUID'] = storedValue;
|
|
150
|
-
} else {
|
|
151
|
-
retryParams['_OverrideUserID'] = storedValue;
|
|
152
|
-
}
|
|
146
|
+
if (stored.userId) {
|
|
147
|
+
log.info(`Using stored user ID: ${stored.userId} (non-interactive mode)`);
|
|
148
|
+
retryParams['_OverrideUserID'] = stored.userId;
|
|
153
149
|
} else {
|
|
154
150
|
log.error(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
155
151
|
log.dim(' Run "dbo login" first, or use an interactive terminal.');
|
|
@@ -167,41 +163,36 @@ export async function checkSubmitErrors(result) {
|
|
|
167
163
|
const prompts = [];
|
|
168
164
|
|
|
169
165
|
if (needsUser) {
|
|
170
|
-
const idType = needsUserUid ? 'UID' : 'ID';
|
|
171
166
|
log.warn(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
172
167
|
log.dim(' Your session may have expired, or you may not be logged in.');
|
|
173
168
|
log.dim(' You can log in with "dbo login" to avoid this prompt in the future.');
|
|
174
169
|
|
|
175
170
|
const stored = await loadUserInfo();
|
|
176
|
-
const storedValue = needsUserUid ? stored.userUid : (stored.userId || stored.userUid);
|
|
177
|
-
const storedLabel = needsUserUid
|
|
178
|
-
? (stored.userUid ? `UID: ${stored.userUid}` : null)
|
|
179
|
-
: (stored.userId ? `ID: ${stored.userId}` : (stored.userUid ? `UID: ${stored.userUid}` : null));
|
|
180
171
|
|
|
181
|
-
if (
|
|
182
|
-
log.dim(` Stored session user ${
|
|
172
|
+
if (stored.userId) {
|
|
173
|
+
log.dim(` Stored session user ID: ${stored.userId}`);
|
|
183
174
|
prompts.push({
|
|
184
175
|
type: 'list',
|
|
185
176
|
name: 'userChoice',
|
|
186
|
-
message:
|
|
177
|
+
message: 'User ID Required:',
|
|
187
178
|
choices: [
|
|
188
|
-
{ name: `Use session user (${
|
|
189
|
-
{ name:
|
|
179
|
+
{ name: `Use session user (ID: ${stored.userId})`, value: stored.userId },
|
|
180
|
+
{ name: 'Enter a different User ID', value: '_custom' },
|
|
190
181
|
],
|
|
191
182
|
});
|
|
192
183
|
prompts.push({
|
|
193
184
|
type: 'input',
|
|
194
185
|
name: 'customUserValue',
|
|
195
|
-
message:
|
|
186
|
+
message: 'Custom User ID:',
|
|
196
187
|
when: (answers) => answers.userChoice === '_custom',
|
|
197
|
-
validate: v => v.trim() ? true :
|
|
188
|
+
validate: v => v.trim() ? true : 'User ID is required',
|
|
198
189
|
});
|
|
199
190
|
} else {
|
|
200
191
|
prompts.push({
|
|
201
192
|
type: 'input',
|
|
202
193
|
name: 'userValue',
|
|
203
|
-
message:
|
|
204
|
-
validate: v => v.trim() ? true :
|
|
194
|
+
message: 'User ID Required:',
|
|
195
|
+
validate: v => v.trim() ? true : 'User ID is required',
|
|
205
196
|
});
|
|
206
197
|
}
|
|
207
198
|
}
|
|
@@ -224,11 +215,7 @@ export async function checkSubmitErrors(result) {
|
|
|
224
215
|
const userValue = answers.userValue
|
|
225
216
|
|| (answers.userChoice === '_custom' ? answers.customUserValue : answers.userChoice);
|
|
226
217
|
if (userValue) {
|
|
227
|
-
|
|
228
|
-
retryParams['_OverrideUserUID'] = userValue.trim();
|
|
229
|
-
} else {
|
|
230
|
-
retryParams['_OverrideUserID'] = userValue.trim();
|
|
231
|
-
}
|
|
218
|
+
retryParams['_OverrideUserID'] = userValue.trim();
|
|
232
219
|
}
|
|
233
220
|
|
|
234
221
|
return retryParams;
|
|
@@ -238,7 +225,7 @@ export async function checkSubmitErrors(result) {
|
|
|
238
225
|
* Handle ticket_error: Record update requires a Ticket ID but none was provided.
|
|
239
226
|
* Prompts the user with 4 recovery options.
|
|
240
227
|
*/
|
|
241
|
-
async function handleTicketError(allText) {
|
|
228
|
+
async function handleTicketError(allText, context = {}) {
|
|
242
229
|
// Try to extract record details from error text
|
|
243
230
|
const entityMatch = allText.match(/entity:(\w+)/);
|
|
244
231
|
const rowIdMatch = allText.match(/RowID:(\d+)/);
|
|
@@ -262,9 +249,63 @@ async function handleTicketError(allText) {
|
|
|
262
249
|
return null;
|
|
263
250
|
}
|
|
264
251
|
|
|
252
|
+
// Fetch ticket suggestions if configured
|
|
253
|
+
let suggestionChoices = [];
|
|
254
|
+
const ticketSuggestionUid = await loadTicketSuggestionOutput();
|
|
255
|
+
const rowUid = context.rowUid || uid || null;
|
|
256
|
+
|
|
257
|
+
if (ticketSuggestionUid && rowUid) {
|
|
258
|
+
try {
|
|
259
|
+
const client = new DboClient();
|
|
260
|
+
const suggestResult = await client.get(`/api/output/${ticketSuggestionUid}`, {
|
|
261
|
+
'_limit': '5',
|
|
262
|
+
'_rowcount': 'false',
|
|
263
|
+
'_template': 'json_raw',
|
|
264
|
+
'_filter@AssetUID': rowUid,
|
|
265
|
+
});
|
|
266
|
+
const rows = suggestResult.payload || suggestResult.data;
|
|
267
|
+
const rowArray = Array.isArray(rows) ? rows : (rows?.Rows || rows?.rows || []);
|
|
268
|
+
for (let i = 0; i < rowArray.length; i++) {
|
|
269
|
+
const row = rowArray[i];
|
|
270
|
+
const ticketId = row.TicketID || row.ticket_id;
|
|
271
|
+
const title = row.Title || row.Name || '';
|
|
272
|
+
const shortName = row.ShortName || row.short_name || '';
|
|
273
|
+
if (ticketId) {
|
|
274
|
+
const label = `${i + 1} (${ticketId}): ${title}${shortName ? ` [${shortName}]` : ''}`;
|
|
275
|
+
suggestionChoices.push({ name: label, value: String(ticketId) });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
log.dim(` (Could not fetch ticket suggestions: ${err.message})`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
265
283
|
log.warn('This record update requires a Ticket ID.');
|
|
266
284
|
|
|
267
285
|
const inquirer = (await import('inquirer')).default;
|
|
286
|
+
|
|
287
|
+
// Build the ticket ID prompt — list with suggestions or manual input
|
|
288
|
+
const needsTicket = (a) => a.ticketAction === 'apply_one' || a.ticketAction === 'apply_all';
|
|
289
|
+
|
|
290
|
+
const ticketIdPrompt = suggestionChoices.length > 0
|
|
291
|
+
? {
|
|
292
|
+
type: 'list',
|
|
293
|
+
name: 'ticketId',
|
|
294
|
+
message: 'Select a Ticket ID:',
|
|
295
|
+
choices: [
|
|
296
|
+
...suggestionChoices,
|
|
297
|
+
{ name: 'Enter a Ticket ID manually\u2026', value: '_manual' },
|
|
298
|
+
],
|
|
299
|
+
when: needsTicket,
|
|
300
|
+
}
|
|
301
|
+
: {
|
|
302
|
+
type: 'input',
|
|
303
|
+
name: 'ticketId',
|
|
304
|
+
message: 'Enter Ticket ID:',
|
|
305
|
+
when: needsTicket,
|
|
306
|
+
validate: v => v.trim() ? true : 'Ticket ID is required',
|
|
307
|
+
};
|
|
308
|
+
|
|
268
309
|
const answers = await inquirer.prompt([
|
|
269
310
|
{
|
|
270
311
|
type: 'list',
|
|
@@ -277,11 +318,12 @@ async function handleTicketError(allText) {
|
|
|
277
318
|
{ name: 'Skip all updates that require a Ticket ID', value: 'skip_all' },
|
|
278
319
|
],
|
|
279
320
|
},
|
|
321
|
+
ticketIdPrompt,
|
|
280
322
|
{
|
|
281
323
|
type: 'input',
|
|
282
|
-
name: '
|
|
283
|
-
message: '
|
|
284
|
-
when: (a) => a
|
|
324
|
+
name: 'ticketIdManual',
|
|
325
|
+
message: 'Ticket ID:',
|
|
326
|
+
when: (a) => needsTicket(a) && a.ticketId === '_manual',
|
|
285
327
|
validate: v => v.trim() ? true : 'Ticket ID is required',
|
|
286
328
|
},
|
|
287
329
|
]);
|
|
@@ -293,7 +335,14 @@ async function handleTicketError(allText) {
|
|
|
293
335
|
return { skipAll: true };
|
|
294
336
|
}
|
|
295
337
|
|
|
296
|
-
const ticketId = answers.
|
|
338
|
+
const ticketId = (answers.ticketIdManual?.trim()) ||
|
|
339
|
+
(answers.ticketId !== '_manual' ? answers.ticketId?.trim() : null);
|
|
340
|
+
|
|
341
|
+
if (!ticketId) {
|
|
342
|
+
log.error(' No Ticket ID provided. Skipping record.');
|
|
343
|
+
return { skipRecord: true };
|
|
344
|
+
}
|
|
345
|
+
|
|
297
346
|
const ticketExpressions = [];
|
|
298
347
|
|
|
299
348
|
if (entity && rowId) {
|
package/src/lib/structure.js
CHANGED
|
@@ -19,6 +19,22 @@ export const DEFAULT_PROJECT_DIRS = [
|
|
|
19
19
|
'Integrations',
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
/** Map from physical output table names → documentation/display names */
|
|
23
|
+
export const OUTPUT_ENTITY_MAP = {
|
|
24
|
+
output: 'output',
|
|
25
|
+
output_value: 'column',
|
|
26
|
+
output_value_filter: 'filter',
|
|
27
|
+
output_value_entity_column_rel: 'join',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** All output hierarchy entity types (physical table names) */
|
|
31
|
+
export const OUTPUT_HIERARCHY_ENTITIES = [
|
|
32
|
+
'output',
|
|
33
|
+
'output_value',
|
|
34
|
+
'output_value_filter',
|
|
35
|
+
'output_value_entity_column_rel',
|
|
36
|
+
];
|
|
37
|
+
|
|
22
38
|
/** Map from server entity key → local project directory name */
|
|
23
39
|
export const ENTITY_DIR_MAP = {
|
|
24
40
|
extension: 'Extensions',
|