@dboio/cli 0.6.13 → 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.
@@ -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 or UID (session not authenticated)
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, _OverrideUserUID, _OverrideUserID, ... }
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
- const storedValue = needsUserUid ? stored.userUid : (stored.userId || stored.userUid);
146
- if (storedValue) {
147
- log.info(`Using stored user ${needsUserUid ? 'UID' : 'ID'}: ${storedValue} (non-interactive mode)`);
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 (storedValue) {
182
- log.dim(` Stored session user ${storedLabel}`);
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: `User ${idType} Required:`,
177
+ message: 'User ID Required:',
187
178
  choices: [
188
- { name: `Use session user (${storedLabel})`, value: storedValue },
189
- { name: `Enter a different User ${idType}`, value: '_custom' },
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: `Custom User ${idType}:`,
186
+ message: 'Custom User ID:',
196
187
  when: (answers) => answers.userChoice === '_custom',
197
- validate: v => v.trim() ? true : `User ${idType} is required`,
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: `User ${idType} Required:`,
204
- validate: v => v.trim() ? true : `User ${idType} is required`,
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
- if (needsUserUid) {
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: 'ticketId',
283
- message: 'Enter Ticket ID:',
284
- when: (a) => a.ticketAction === 'apply_one' || a.ticketAction === 'apply_all',
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.ticketId.trim();
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) {
@@ -0,0 +1,62 @@
1
+ import { mkdir, stat, writeFile, access } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { DEFAULT_PROJECT_DIRS } from './structure.js';
4
+ import { log } from './logger.js';
5
+
6
+ /**
7
+ * Scaffold the standard DBO project directory structure in cwd.
8
+ * Creates missing directories, skips existing ones, warns on name conflicts.
9
+ * Also creates app.json with {} if absent.
10
+ *
11
+ * @param {string} [cwd=process.cwd()]
12
+ * @returns {Promise<{ created: string[], skipped: string[], warned: string[] }>}
13
+ */
14
+ export async function scaffoldProjectDirs(cwd = process.cwd()) {
15
+ const created = [];
16
+ const skipped = [];
17
+ const warned = [];
18
+
19
+ for (const dir of DEFAULT_PROJECT_DIRS) {
20
+ const target = join(cwd, dir);
21
+ try {
22
+ const s = await stat(target);
23
+ if (s.isDirectory()) {
24
+ skipped.push(dir);
25
+ } else {
26
+ log.warn(` Skipping "${dir}" — a file with that name already exists`);
27
+ warned.push(dir);
28
+ }
29
+ } catch {
30
+ // Does not exist — create it
31
+ await mkdir(target, { recursive: true });
32
+ created.push(dir);
33
+ }
34
+ }
35
+
36
+ // Create app.json if absent
37
+ const appJsonPath = join(cwd, 'app.json');
38
+ try {
39
+ await access(appJsonPath);
40
+ } catch {
41
+ await writeFile(appJsonPath, '{}\n');
42
+ created.push('app.json');
43
+ }
44
+
45
+ return { created, skipped, warned };
46
+ }
47
+
48
+ /**
49
+ * Print the scaffold summary to the console.
50
+ * @param {{ created: string[], skipped: string[], warned: string[] }} result
51
+ */
52
+ export function logScaffoldResult({ created, skipped, warned }) {
53
+ if (created.length) {
54
+ log.success('Scaffolded directories:');
55
+ for (const d of created) log.dim(` + ${d}`);
56
+ }
57
+ if (skipped.length) {
58
+ log.dim('Skipped (already exist):');
59
+ for (const d of skipped) log.dim(` · ${d}`);
60
+ }
61
+ // warned items already printed inline during scaffold
62
+ }
@@ -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',