@dboio/cli 0.6.12 → 0.6.13
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 +9 -0
- package/package.json +1 -1
- package/src/commands/add.js +1 -0
- package/src/commands/clone.js +23 -1
- package/src/commands/content.js +6 -2
- package/src/commands/deploy.js +9 -3
- package/src/commands/init.js +19 -1
- package/src/commands/input.js +1 -0
- package/src/commands/push.js +45 -17
- package/src/commands/status.js +4 -1
- package/src/lib/config.js +25 -0
- package/src/lib/transaction-key.js +46 -0
package/README.md
CHANGED
|
@@ -157,6 +157,7 @@ All configuration is **directory-scoped**. Each project folder maintains its own
|
|
|
157
157
|
| `AppName` | string | App display name |
|
|
158
158
|
| `AppShortName` | string | App short name (used for `dbo clone --app`) |
|
|
159
159
|
| `AppModifyKey` | string | ModifyKey for locked/production apps (set by `dbo clone`, used for submission guards) |
|
|
160
|
+
| `TransactionKeyPreset` | `RowUID` \| `RowID` | Row key type for auto-assembled expressions (set during `dbo init`/`dbo clone`, default `RowUID`) |
|
|
160
161
|
| `ContentPlacement` | `bin` \| `path` \| `ask` | Where to place content files during clone |
|
|
161
162
|
| `MediaPlacement` | `bin` \| `fullpath` \| `ask` | Where to place media files during clone |
|
|
162
163
|
| `<Entity>FilenameCol` | column name | Filename column for entity-dir records (e.g., `ExtensionFilenameCol`) |
|
|
@@ -489,6 +490,7 @@ dbo input -d 'RowUID:abc;column:content.Content@file.css' -v
|
|
|
489
490
|
| `-C, --confirm <true\|false>` | Commit changes (default: `true`). Use `false` for validation only |
|
|
490
491
|
| `--ticket <id>` | Override ticket ID (`_OverrideTicketID`) |
|
|
491
492
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
493
|
+
| `--row-key <type>` | Row key type (`RowUID` or `RowID`) — no-op for `-d` passthrough, available for consistency |
|
|
492
494
|
| `--login` | Auto-login user created by this submission |
|
|
493
495
|
| `--transactional` | Use transactional processing |
|
|
494
496
|
| `--json` | Output raw JSON response |
|
|
@@ -634,6 +636,7 @@ dbo input -d 'RowUID:albain3dwkofbhnd1qtd1q;column:content.Content@assets/css/co
|
|
|
634
636
|
| `--ticket <id>` | Override ticket ID |
|
|
635
637
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
636
638
|
| `--multipart` | Use multipart/form-data upload (for binary files) |
|
|
639
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
637
640
|
|
|
638
641
|
#### `dbo content pull`
|
|
639
642
|
|
|
@@ -945,6 +948,7 @@ The `@colors.css` reference tells push to read the content from that file. All o
|
|
|
945
948
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
946
949
|
| `--ticket <id>` | Override ticket ID |
|
|
947
950
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
951
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
948
952
|
| `--meta-only` | Only push metadata, skip file content |
|
|
949
953
|
| `--content-only` | Only push file content, skip metadata |
|
|
950
954
|
| `-y, --yes` | Auto-accept all prompts |
|
|
@@ -1108,6 +1112,7 @@ The `@colors.css` reference tells the CLI to read the file content from `colors.
|
|
|
1108
1112
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1109
1113
|
| `--ticket <id>` | Override ticket ID |
|
|
1110
1114
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1115
|
+
| `--row-key <type>` | Row key type (`RowUID` or `RowID`) — `add` always uses `RowID:add1` for new records regardless |
|
|
1111
1116
|
| `-y, --yes` | Auto-accept all prompts |
|
|
1112
1117
|
| `--json` | Output raw JSON |
|
|
1113
1118
|
| `--jq <expr>` | Filter JSON response |
|
|
@@ -1328,6 +1333,9 @@ dbo deploy js:app --ticket abc123
|
|
|
1328
1333
|
|
|
1329
1334
|
# Validate without deploying
|
|
1330
1335
|
dbo deploy css:colors --confirm false
|
|
1336
|
+
|
|
1337
|
+
# Deploy a multipart (binary) entry from manifest
|
|
1338
|
+
dbo deploy img:logo
|
|
1331
1339
|
```
|
|
1332
1340
|
|
|
1333
1341
|
| Flag | Description |
|
|
@@ -1337,6 +1345,7 @@ dbo deploy css:colors --confirm false
|
|
|
1337
1345
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1338
1346
|
| `--ticket <id>` | Override ticket ID |
|
|
1339
1347
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1348
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
1340
1349
|
| `--json` | Output raw JSON |
|
|
1341
1350
|
| `-v, --verbose` | Show HTTP request details |
|
|
1342
1351
|
| `--domain <host>` | Override domain |
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -19,6 +19,7 @@ export const addCommand = new Command('add')
|
|
|
19
19
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
20
20
|
.option('--ticket <id>', 'Override ticket ID')
|
|
21
21
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
22
|
+
.option('--row-key <type>', 'Row key type (RowUID or RowID) — add uses RowID:add1 for new records regardless')
|
|
22
23
|
.option('-y, --yes', 'Auto-accept all prompts')
|
|
23
24
|
.option('--json', 'Output raw JSON')
|
|
24
25
|
.option('--jq <expr>', 'Filter JSON response')
|
package/src/commands/clone.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
3
3
|
import { join, basename, extname } from 'path';
|
|
4
4
|
import { DboClient } from '../lib/client.js';
|
|
5
|
-
import { loadConfig, updateConfigWithApp, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize, saveAppModifyKey } from '../lib/config.js';
|
|
5
|
+
import { loadConfig, updateConfigWithApp, loadClonePlacement, saveClonePlacement, ensureGitignore, saveEntityDirPreference, loadEntityDirPreference, saveEntityContentExtractions, loadEntityContentExtractions, saveAppJsonBaseline, addDeleteEntry, loadCollisionResolutions, saveCollisionResolutions, loadSynchronize, saveAppModifyKey, loadTransactionKeyPreset, saveTransactionKeyPreset } from '../lib/config.js';
|
|
6
6
|
import { buildBinHierarchy, resolveBinPath, createDirectories, saveStructureFile, getBinName, findBinByPath, BINS_DIR, DEFAULT_PROJECT_DIRS, ENTITY_DIR_MAP } from '../lib/structure.js';
|
|
7
7
|
import { log } from '../lib/logger.js';
|
|
8
8
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
@@ -511,6 +511,28 @@ export async function performClone(source, options = {}) {
|
|
|
511
511
|
log.warn('');
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
// Prompt for TransactionKeyPreset if not already set
|
|
515
|
+
const existingPreset = await loadTransactionKeyPreset();
|
|
516
|
+
if (!existingPreset) {
|
|
517
|
+
if (options.yes || !process.stdin.isTTY) {
|
|
518
|
+
await saveTransactionKeyPreset('RowUID');
|
|
519
|
+
log.dim(' TransactionKeyPreset: RowUID (default)');
|
|
520
|
+
} else {
|
|
521
|
+
const inquirer = (await import('inquirer')).default;
|
|
522
|
+
const { preset } = await inquirer.prompt([{
|
|
523
|
+
type: 'list',
|
|
524
|
+
name: 'preset',
|
|
525
|
+
message: 'Which row key should the CLI use when building input expressions?',
|
|
526
|
+
choices: [
|
|
527
|
+
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
528
|
+
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
529
|
+
],
|
|
530
|
+
}]);
|
|
531
|
+
await saveTransactionKeyPreset(preset);
|
|
532
|
+
log.dim(` TransactionKeyPreset: ${preset}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
514
536
|
// Step 3: Update package.json
|
|
515
537
|
await updatePackageJson(appJson, config);
|
|
516
538
|
|
package/src/commands/content.js
CHANGED
|
@@ -7,6 +7,7 @@ import { formatResponse, formatError } from '../lib/formatter.js';
|
|
|
7
7
|
import { saveToDisk } from '../lib/save-to-disk.js';
|
|
8
8
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
9
9
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
|
|
10
|
+
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
10
11
|
import { log } from '../lib/logger.js';
|
|
11
12
|
|
|
12
13
|
function collect(value, previous) {
|
|
@@ -32,6 +33,7 @@ const deployCmd = new Command('deploy')
|
|
|
32
33
|
.option('--ticket <id>', 'Override ticket ID')
|
|
33
34
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
34
35
|
.option('--multipart', 'Use multipart/form-data upload (for binary files)')
|
|
36
|
+
.option('--row-key <type>', 'Override row key type for this invocation (RowUID or RowID)')
|
|
35
37
|
.option('--json', 'Output raw JSON')
|
|
36
38
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
37
39
|
.option('--domain <host>', 'Override domain')
|
|
@@ -63,12 +65,14 @@ const deployCmd = new Command('deploy')
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
66
69
|
const extraParams = { '_confirm': options.confirm };
|
|
67
70
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
68
71
|
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
69
72
|
|
|
70
73
|
const isMultipart = options.multipart === true;
|
|
71
|
-
const
|
|
74
|
+
const rowKeyExpr = transactionKey === 'RowID' ? `RowID:${uid}` : `RowUID:${uid}`;
|
|
75
|
+
const dataExprs = isMultipart ? [] : [`${rowKeyExpr};column:content.Content@${filepath}`];
|
|
72
76
|
|
|
73
77
|
// Apply stored ticket if no --ticket flag
|
|
74
78
|
if (!options.ticket) {
|
|
@@ -90,7 +94,7 @@ const deployCmd = new Command('deploy')
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
const files = [{
|
|
93
|
-
fieldName:
|
|
97
|
+
fieldName: `${rowKeyExpr};column:content.Content`,
|
|
94
98
|
filePath: filepath,
|
|
95
99
|
fileName: filepath.split('/').pop(),
|
|
96
100
|
}];
|
package/src/commands/deploy.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
|
|
|
5
5
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
6
6
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
7
7
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
|
|
8
|
+
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
8
9
|
import { log } from '../lib/logger.js';
|
|
9
10
|
|
|
10
11
|
const MANIFEST_FILE = 'dbo.deploy.json';
|
|
@@ -16,6 +17,7 @@ export const deployCommand = new Command('deploy')
|
|
|
16
17
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
17
18
|
.option('--ticket <id>', 'Override ticket ID')
|
|
18
19
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
20
|
+
.option('--row-key <type>', 'Override row key type for this invocation (RowUID or RowID)')
|
|
19
21
|
.option('--json', 'Output raw JSON')
|
|
20
22
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
21
23
|
.option('--domain <host>', 'Override domain')
|
|
@@ -54,6 +56,9 @@ export const deployCommand = new Command('deploy')
|
|
|
54
56
|
process.exit(1);
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// Resolve transaction key preset once before the entry loop
|
|
60
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
61
|
+
|
|
57
62
|
// ModifyKey guard — check once before any submissions
|
|
58
63
|
const modifyKeyResult = await checkModifyKey(options);
|
|
59
64
|
if (modifyKeyResult.cancel) {
|
|
@@ -103,9 +108,10 @@ export const deployCommand = new Command('deploy')
|
|
|
103
108
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
104
109
|
if (activeModifyKey) extraParams['_modify_key'] = activeModifyKey;
|
|
105
110
|
|
|
111
|
+
const rowKeyExpr = transactionKey === 'RowID' ? `RowID:${uid}` : `RowUID:${uid}`;
|
|
106
112
|
const dataExprs = isMultipart
|
|
107
|
-
? [
|
|
108
|
-
: [
|
|
113
|
+
? [`${rowKeyExpr};column:${entity}.Filename=${filename}`]
|
|
114
|
+
: [`${rowKeyExpr};column:${entity}.${column}@${file}`];
|
|
109
115
|
|
|
110
116
|
// Apply stored ticket if no --ticket flag
|
|
111
117
|
if (!options.ticket) {
|
|
@@ -127,7 +133,7 @@ export const deployCommand = new Command('deploy')
|
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
const files = [{
|
|
130
|
-
fieldName:
|
|
136
|
+
fieldName: `${rowKeyExpr};column:${entity}.${column}`,
|
|
131
137
|
filePath: file,
|
|
132
138
|
fileName: file.split('/').pop(),
|
|
133
139
|
}];
|
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { access } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore } from '../lib/config.js';
|
|
4
|
+
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset } from '../lib/config.js';
|
|
5
5
|
import { installOrUpdateClaudeCommands } from './install.js';
|
|
6
6
|
import { log } from '../lib/logger.js';
|
|
7
7
|
|
|
@@ -67,6 +67,24 @@ export const initCommand = new Command('init')
|
|
|
67
67
|
log.success(`Initialized .dbo/ for ${domain}`);
|
|
68
68
|
log.dim(' Run "dbo login" to authenticate.');
|
|
69
69
|
|
|
70
|
+
// Prompt for TransactionKeyPreset
|
|
71
|
+
if (!options.nonInteractive) {
|
|
72
|
+
const inquirer = (await import('inquirer')).default;
|
|
73
|
+
const { preset } = await inquirer.prompt([{
|
|
74
|
+
type: 'list',
|
|
75
|
+
name: 'preset',
|
|
76
|
+
message: 'Which row key should the CLI use when building input expressions?',
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
79
|
+
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
80
|
+
],
|
|
81
|
+
}]);
|
|
82
|
+
await saveTransactionKeyPreset(preset);
|
|
83
|
+
log.dim(` TransactionKeyPreset: ${preset}`);
|
|
84
|
+
} else {
|
|
85
|
+
await saveTransactionKeyPreset('RowUID');
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
// Clone if requested
|
|
71
89
|
if (options.clone || options.app) {
|
|
72
90
|
let appShortName = options.app;
|
package/src/commands/input.js
CHANGED
|
@@ -18,6 +18,7 @@ export const inputCommand = new Command('input')
|
|
|
18
18
|
.option('-C, --confirm <value>', 'Commit changes: true (default) or false for validation only', 'true')
|
|
19
19
|
.option('--ticket <id>', 'Override ticket ID (_OverrideTicketID)')
|
|
20
20
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
21
|
+
.option('--row-key <type>', 'Row key type (RowUID or RowID) — no-op for -d passthrough, available for consistency')
|
|
21
22
|
.option('--login', 'Auto-login user created by this submission')
|
|
22
23
|
.option('--transactional', 'Use transactional processing')
|
|
23
24
|
.option('--json', 'Output raw JSON response')
|
package/src/commands/push.js
CHANGED
|
@@ -9,6 +9,7 @@ import { shouldSkipColumn } from '../lib/columns.js';
|
|
|
9
9
|
import { loadConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline } from '../lib/config.js';
|
|
10
10
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
11
11
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
12
|
+
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
12
13
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
13
14
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
14
15
|
import { detectChangedColumns, findBaselineEntry } from '../lib/delta.js';
|
|
@@ -23,6 +24,7 @@ export const pushCommand = new Command('push')
|
|
|
23
24
|
.option('--meta-only', 'Only push metadata changes, skip file content')
|
|
24
25
|
.option('--content-only', 'Only push file content, skip metadata columns')
|
|
25
26
|
.option('-y, --yes', 'Auto-accept all prompts (path refactoring, etc.)')
|
|
27
|
+
.option('--row-key <type>', 'Override row key type for this invocation (RowUID or RowID)')
|
|
26
28
|
.option('--json', 'Output raw JSON')
|
|
27
29
|
.option('--jq <expr>', 'Filter JSON response')
|
|
28
30
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
@@ -39,15 +41,18 @@ export const pushCommand = new Command('push')
|
|
|
39
41
|
}
|
|
40
42
|
const modifyKey = modifyKeyResult.modifyKey;
|
|
41
43
|
|
|
44
|
+
// Resolve transaction key preset
|
|
45
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
46
|
+
|
|
42
47
|
// Process pending deletions from synchronize.json
|
|
43
|
-
await processPendingDeletes(client, options, modifyKey);
|
|
48
|
+
await processPendingDeletes(client, options, modifyKey, transactionKey);
|
|
44
49
|
|
|
45
50
|
const pathStat = await stat(targetPath);
|
|
46
51
|
|
|
47
52
|
if (pathStat.isDirectory()) {
|
|
48
|
-
await pushDirectory(targetPath, client, options, modifyKey);
|
|
53
|
+
await pushDirectory(targetPath, client, options, modifyKey, transactionKey);
|
|
49
54
|
} else {
|
|
50
|
-
await pushSingleFile(targetPath, client, options, modifyKey);
|
|
55
|
+
await pushSingleFile(targetPath, client, options, modifyKey, transactionKey);
|
|
51
56
|
}
|
|
52
57
|
} catch (err) {
|
|
53
58
|
formatError(err);
|
|
@@ -58,7 +63,7 @@ export const pushCommand = new Command('push')
|
|
|
58
63
|
/**
|
|
59
64
|
* Process pending delete entries from .dbo/synchronize.json
|
|
60
65
|
*/
|
|
61
|
-
async function processPendingDeletes(client, options, modifyKey = null) {
|
|
66
|
+
async function processPendingDeletes(client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
62
67
|
const sync = await loadSynchronize();
|
|
63
68
|
if (!sync.delete || sync.delete.length === 0) return;
|
|
64
69
|
|
|
@@ -140,7 +145,7 @@ async function processPendingDeletes(client, options, modifyKey = null) {
|
|
|
140
145
|
/**
|
|
141
146
|
* Push a single file using its companion .metadata.json
|
|
142
147
|
*/
|
|
143
|
-
async function pushSingleFile(filePath, client, options, modifyKey = null) {
|
|
148
|
+
async function pushSingleFile(filePath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
144
149
|
// Find the metadata file
|
|
145
150
|
const dir = dirname(filePath);
|
|
146
151
|
const base = basename(filePath, extname(filePath));
|
|
@@ -154,13 +159,13 @@ async function pushSingleFile(filePath, client, options, modifyKey = null) {
|
|
|
154
159
|
process.exit(1);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey);
|
|
162
|
+
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
/**
|
|
161
166
|
* Push all records found in a directory (recursive)
|
|
162
167
|
*/
|
|
163
|
-
async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
168
|
+
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
164
169
|
const metaFiles = await findMetadataFiles(dirPath);
|
|
165
170
|
|
|
166
171
|
if (metaFiles.length === 0) {
|
|
@@ -272,7 +277,7 @@ async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
|
272
277
|
|
|
273
278
|
for (const item of toPush) {
|
|
274
279
|
try {
|
|
275
|
-
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey);
|
|
280
|
+
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey, transactionKey);
|
|
276
281
|
if (success) {
|
|
277
282
|
succeeded++;
|
|
278
283
|
successfulPushes.push(item);
|
|
@@ -306,14 +311,37 @@ async function pushDirectory(dirPath, client, options, modifyKey = null) {
|
|
|
306
311
|
* @param {string[]|null} changedColumns - Optional array of changed column names (for delta sync)
|
|
307
312
|
* @returns {Promise<boolean>} - True if push succeeded
|
|
308
313
|
*/
|
|
309
|
-
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null, modifyKey = null) {
|
|
310
|
-
const uid = meta.UID
|
|
314
|
+
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null, modifyKey = null, transactionKey = 'RowUID') {
|
|
315
|
+
const uid = meta.UID;
|
|
316
|
+
const id = meta._id;
|
|
311
317
|
const entity = meta._entity;
|
|
312
318
|
const contentCols = new Set(meta._contentColumns || []);
|
|
313
319
|
const metaDir = dirname(metaPath);
|
|
314
320
|
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
// Determine the row key prefix and value based on the preset
|
|
322
|
+
let rowKeyPrefix, rowKeyValue;
|
|
323
|
+
if (transactionKey === 'RowID') {
|
|
324
|
+
if (id) {
|
|
325
|
+
rowKeyPrefix = 'RowID';
|
|
326
|
+
rowKeyValue = id;
|
|
327
|
+
} else {
|
|
328
|
+
log.warn(` ⚠ Preset is RowID but no _id found in ${basename(metaPath)} — falling back to RowUID`);
|
|
329
|
+
rowKeyPrefix = 'RowUID';
|
|
330
|
+
rowKeyValue = uid;
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
if (uid) {
|
|
334
|
+
rowKeyPrefix = 'RowUID';
|
|
335
|
+
rowKeyValue = uid;
|
|
336
|
+
} else if (id) {
|
|
337
|
+
log.warn(` ⚠ Preset is RowUID but no UID found in ${basename(metaPath)} — falling back to RowID`);
|
|
338
|
+
rowKeyPrefix = 'RowID';
|
|
339
|
+
rowKeyValue = id;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!rowKeyValue) {
|
|
344
|
+
throw new Error(`No UID or _id found in ${metaPath}`);
|
|
317
345
|
}
|
|
318
346
|
if (!entity) {
|
|
319
347
|
throw new Error(`No _entity found in ${metaPath}`);
|
|
@@ -351,9 +379,9 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
351
379
|
// @filename reference — resolve to actual file path
|
|
352
380
|
const refFile = strValue.substring(1);
|
|
353
381
|
const refPath = join(metaDir, refFile);
|
|
354
|
-
dataExprs.push(
|
|
382
|
+
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${entity}.${key}@${refPath}`);
|
|
355
383
|
} else {
|
|
356
|
-
dataExprs.push(
|
|
384
|
+
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${entity}.${key}=${strValue}`);
|
|
357
385
|
}
|
|
358
386
|
}
|
|
359
387
|
|
|
@@ -363,10 +391,10 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
363
391
|
}
|
|
364
392
|
|
|
365
393
|
const fieldLabel = changedColumns ? `${dataExprs.length} changed field(s)` : `${dataExprs.length} field(s)`;
|
|
366
|
-
log.info(`Pushing ${basename(metaPath, '.metadata.json')} (${entity}:${
|
|
394
|
+
log.info(`Pushing ${basename(metaPath, '.metadata.json')} (${entity}:${rowKeyValue}) — ${fieldLabel}`);
|
|
367
395
|
|
|
368
396
|
// Apply stored ticket if no --ticket flag
|
|
369
|
-
await applyStoredTicketToSubmission(dataExprs, entity, uid, uid, options);
|
|
397
|
+
await applyStoredTicketToSubmission(dataExprs, entity, uid || id, uid || id, options);
|
|
370
398
|
|
|
371
399
|
const extraParams = { '_confirm': options.confirm };
|
|
372
400
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
@@ -422,7 +450,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
422
450
|
}
|
|
423
451
|
|
|
424
452
|
// Clean up per-record ticket on success
|
|
425
|
-
await clearRecordTicket(uid);
|
|
453
|
+
await clearRecordTicket(uid || id);
|
|
426
454
|
|
|
427
455
|
// Update file timestamps from server response
|
|
428
456
|
try {
|
package/src/commands/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { loadConfig, isInitialized, getActiveCookiesPath, loadUserInfo, getAllPluginScopes } from '../lib/config.js';
|
|
2
|
+
import { loadConfig, isInitialized, getActiveCookiesPath, loadUserInfo, getAllPluginScopes, loadTransactionKeyPreset } from '../lib/config.js';
|
|
3
3
|
import { loadCookies } from '../lib/cookie-jar.js';
|
|
4
4
|
import { access } from 'fs/promises';
|
|
5
5
|
import { join } from 'path';
|
|
@@ -38,6 +38,9 @@ export const statusCommand = new Command('status')
|
|
|
38
38
|
log.label('Session', 'No active session. Run "dbo login".');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const transactionKeyPreset = await loadTransactionKeyPreset();
|
|
42
|
+
log.label('Transaction Key', transactionKeyPreset || '(not set — defaults to RowUID)');
|
|
43
|
+
|
|
41
44
|
// Display plugin status
|
|
42
45
|
const scopes = await getAllPluginScopes();
|
|
43
46
|
const pluginNames = Object.keys(scopes);
|
package/src/lib/config.js
CHANGED
|
@@ -555,6 +555,31 @@ export async function loadAppModifyKey() {
|
|
|
555
555
|
} catch { return null; }
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
// ─── TransactionKeyPreset ─────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Save TransactionKeyPreset to .dbo/config.json.
|
|
562
|
+
* @param {'RowUID'|'RowID'} preset
|
|
563
|
+
*/
|
|
564
|
+
export async function saveTransactionKeyPreset(preset) {
|
|
565
|
+
await mkdir(dboDir(), { recursive: true });
|
|
566
|
+
let existing = {};
|
|
567
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
568
|
+
existing.TransactionKeyPreset = preset;
|
|
569
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Load TransactionKeyPreset from .dbo/config.json.
|
|
574
|
+
* Returns 'RowUID', 'RowID', or null if not set.
|
|
575
|
+
*/
|
|
576
|
+
export async function loadTransactionKeyPreset() {
|
|
577
|
+
try {
|
|
578
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
579
|
+
return JSON.parse(raw).TransactionKeyPreset || null;
|
|
580
|
+
} catch { return null; }
|
|
581
|
+
}
|
|
582
|
+
|
|
558
583
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|
|
559
584
|
|
|
560
585
|
/**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { loadTransactionKeyPreset, saveTransactionKeyPreset } from './config.js';
|
|
2
|
+
import { log } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the active transaction key preset.
|
|
6
|
+
* Priority: --row-key flag > config > lazy prompt > default RowUID.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Command options (may have .rowKey)
|
|
9
|
+
* @returns {Promise<'RowUID'|'RowID'>}
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveTransactionKey(options = {}) {
|
|
12
|
+
// 1. --row-key flag takes precedence (no config write)
|
|
13
|
+
if (options.rowKey) return options.rowKey;
|
|
14
|
+
|
|
15
|
+
// 2. Check config
|
|
16
|
+
const stored = await loadTransactionKeyPreset();
|
|
17
|
+
if (stored) return stored;
|
|
18
|
+
|
|
19
|
+
// 3. Non-interactive or -y: default to RowUID, save to config
|
|
20
|
+
if (options.yes || options.nonInteractive || !process.stdin.isTTY) {
|
|
21
|
+
await saveTransactionKeyPreset('RowUID');
|
|
22
|
+
return 'RowUID';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 4. Lazy prompt (first submission without preset configured)
|
|
26
|
+
log.plain('');
|
|
27
|
+
log.info('TransactionKeyPreset is not configured.');
|
|
28
|
+
log.dim(' RowUID — stable across domains (recommended)');
|
|
29
|
+
log.dim(' RowID — uses numeric IDs (may differ across domains)');
|
|
30
|
+
log.plain('');
|
|
31
|
+
|
|
32
|
+
const inquirer = (await import('inquirer')).default;
|
|
33
|
+
const { preset } = await inquirer.prompt([{
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'preset',
|
|
36
|
+
message: 'Which row key should the CLI use when building input expressions?',
|
|
37
|
+
choices: [
|
|
38
|
+
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
39
|
+
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
40
|
+
],
|
|
41
|
+
}]);
|
|
42
|
+
|
|
43
|
+
await saveTransactionKeyPreset(preset);
|
|
44
|
+
log.dim(` Saved TransactionKeyPreset: ${preset} to .dbo/config.json`);
|
|
45
|
+
return preset;
|
|
46
|
+
}
|