@dboio/cli 0.6.11 → 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 +26 -2
- package/package.json +1 -1
- package/src/commands/add.js +1 -0
- package/src/commands/clone.js +23 -1
- package/src/commands/content.js +32 -7
- package/src/commands/deploy.js +38 -9
- 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 |
|
|
@@ -618,6 +620,7 @@ dbo content deploy albain3dwkofbhnd1qtd1q assets/css/colors.css
|
|
|
618
620
|
dbo content deploy rncjivlghu65bmkbjnxynq assets/js/app.js
|
|
619
621
|
dbo content deploy abc123 docs/readme.md --ticket myTicket
|
|
620
622
|
dbo content deploy abc123 test.html --confirm false # validate only
|
|
623
|
+
dbo content deploy abc123 image.png --multipart # binary file upload
|
|
621
624
|
```
|
|
622
625
|
|
|
623
626
|
This is a shorthand for:
|
|
@@ -632,6 +635,8 @@ dbo input -d 'RowUID:albain3dwkofbhnd1qtd1q;column:content.Content@assets/css/co
|
|
|
632
635
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
633
636
|
| `--ticket <id>` | Override ticket ID |
|
|
634
637
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
638
|
+
| `--multipart` | Use multipart/form-data upload (for binary files) |
|
|
639
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
635
640
|
|
|
636
641
|
#### `dbo content pull`
|
|
637
642
|
|
|
@@ -943,6 +948,7 @@ The `@colors.css` reference tells push to read the content from that file. All o
|
|
|
943
948
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
944
949
|
| `--ticket <id>` | Override ticket ID |
|
|
945
950
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
951
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
946
952
|
| `--meta-only` | Only push metadata, skip file content |
|
|
947
953
|
| `--content-only` | Only push file content, skip metadata |
|
|
948
954
|
| `-y, --yes` | Auto-accept all prompts |
|
|
@@ -1106,6 +1112,7 @@ The `@colors.css` reference tells the CLI to read the file content from `colors.
|
|
|
1106
1112
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1107
1113
|
| `--ticket <id>` | Override ticket ID |
|
|
1108
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 |
|
|
1109
1116
|
| `-y, --yes` | Auto-accept all prompts |
|
|
1110
1117
|
| `--json` | Output raw JSON |
|
|
1111
1118
|
| `--jq <expr>` | Filter JSON response |
|
|
@@ -1326,6 +1333,9 @@ dbo deploy js:app --ticket abc123
|
|
|
1326
1333
|
|
|
1327
1334
|
# Validate without deploying
|
|
1328
1335
|
dbo deploy css:colors --confirm false
|
|
1336
|
+
|
|
1337
|
+
# Deploy a multipart (binary) entry from manifest
|
|
1338
|
+
dbo deploy img:logo
|
|
1329
1339
|
```
|
|
1330
1340
|
|
|
1331
1341
|
| Flag | Description |
|
|
@@ -1335,6 +1345,7 @@ dbo deploy css:colors --confirm false
|
|
|
1335
1345
|
| `-C, --confirm <true\|false>` | Commit (default: `true`) |
|
|
1336
1346
|
| `--ticket <id>` | Override ticket ID |
|
|
1337
1347
|
| `--modify-key <key>` | Provide ModifyKey directly (skips interactive prompt) |
|
|
1348
|
+
| `--row-key <type>` | Override row key type for this invocation (`RowUID` or `RowID`) |
|
|
1338
1349
|
| `--json` | Output raw JSON |
|
|
1339
1350
|
| `-v, --verbose` | Show HTTP request details |
|
|
1340
1351
|
| `--domain <host>` | Override domain |
|
|
@@ -1361,6 +1372,17 @@ Create a `dbo.deploy.json` file in your project root to define named deployments
|
|
|
1361
1372
|
"file": "docs/colors.md",
|
|
1362
1373
|
"entity": "extension",
|
|
1363
1374
|
"column": "Text"
|
|
1375
|
+
},
|
|
1376
|
+
"img:logo": {
|
|
1377
|
+
"uid": "x9fk2m3npqrs7tuvwyz1ab",
|
|
1378
|
+
"file": "assets/images/logo.png",
|
|
1379
|
+
"multipart": true
|
|
1380
|
+
},
|
|
1381
|
+
"upload:icons": {
|
|
1382
|
+
"uid": "7ddf10982a96457fa4f440",
|
|
1383
|
+
"file": "assets/css/launchpad-icons.css",
|
|
1384
|
+
"multipart": true,
|
|
1385
|
+
"filename": "launchpad-icons.css"
|
|
1364
1386
|
}
|
|
1365
1387
|
}
|
|
1366
1388
|
}
|
|
@@ -1370,8 +1392,10 @@ Create a `dbo.deploy.json` file in your project root to define named deployments
|
|
|
1370
1392
|
|-------|-------------|---------|
|
|
1371
1393
|
| `uid` | Target record UID (required) | — |
|
|
1372
1394
|
| `file` | Local file path (required) | — |
|
|
1373
|
-
| `entity` | Target entity name | `content` |
|
|
1374
|
-
| `column` | Target column name | `Content` |
|
|
1395
|
+
| `entity` | Target entity name | `content` (`media` when `multipart: true`) |
|
|
1396
|
+
| `column` | Target column name | `Content` (`File` when `multipart: true`) |
|
|
1397
|
+
| `multipart` | Use multipart/form-data upload (for binary files) | `false` |
|
|
1398
|
+
| `filename` | Set the `Filename` column on the target record | basename of `file` |
|
|
1375
1399
|
|
|
1376
1400
|
This replaces the curl commands typically embedded in `package.json` scripts.
|
|
1377
1401
|
|
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) {
|
|
@@ -31,6 +32,8 @@ const deployCmd = new Command('deploy')
|
|
|
31
32
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
32
33
|
.option('--ticket <id>', 'Override ticket ID')
|
|
33
34
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
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)')
|
|
34
37
|
.option('--json', 'Output raw JSON')
|
|
35
38
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
36
39
|
.option('--domain <host>', 'Override domain')
|
|
@@ -62,11 +65,14 @@ const deployCmd = new Command('deploy')
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
const transactionKey = await resolveTransactionKey(options);
|
|
65
69
|
const extraParams = { '_confirm': options.confirm };
|
|
66
70
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
67
71
|
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
68
72
|
|
|
69
|
-
const
|
|
73
|
+
const isMultipart = options.multipart === true;
|
|
74
|
+
const rowKeyExpr = transactionKey === 'RowID' ? `RowID:${uid}` : `RowUID:${uid}`;
|
|
75
|
+
const dataExprs = isMultipart ? [] : [`${rowKeyExpr};column:content.Content@${filepath}`];
|
|
70
76
|
|
|
71
77
|
// Apply stored ticket if no --ticket flag
|
|
72
78
|
if (!options.ticket) {
|
|
@@ -77,16 +83,36 @@ const deployCmd = new Command('deploy')
|
|
|
77
83
|
}
|
|
78
84
|
await applyStoredTicketToSubmission(dataExprs, 'content', uid, uid, options, sessionTicketOverride);
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
// Submit helper — handles both URL-encoded and multipart modes
|
|
87
|
+
async function submit() {
|
|
88
|
+
if (isMultipart) {
|
|
89
|
+
const fields = { ...extraParams };
|
|
90
|
+
for (const expr of dataExprs) {
|
|
91
|
+
const eqIdx = expr.indexOf('=');
|
|
92
|
+
if (eqIdx !== -1) {
|
|
93
|
+
fields[expr.substring(0, eqIdx)] = expr.substring(eqIdx + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const files = [{
|
|
97
|
+
fieldName: `${rowKeyExpr};column:content.Content`,
|
|
98
|
+
filePath: filepath,
|
|
99
|
+
fileName: filepath.split('/').pop(),
|
|
100
|
+
}];
|
|
101
|
+
return client.postMultipart('/api/input/submit', fields, files);
|
|
102
|
+
} else {
|
|
103
|
+
const body = await buildInputBody(dataExprs, extraParams);
|
|
104
|
+
return client.postUrlEncoded('/api/input/submit', body);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result = await submit();
|
|
82
109
|
|
|
83
110
|
// Reactive ModifyKey retry
|
|
84
111
|
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
85
112
|
const retryMK = await handleModifyKeyError();
|
|
86
113
|
if (retryMK.cancel) { log.info('Submission cancelled'); return; }
|
|
87
114
|
extraParams['_modify_key'] = retryMK.modifyKey;
|
|
88
|
-
|
|
89
|
-
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
115
|
+
result = await submit();
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
@@ -101,8 +127,7 @@ const deployCmd = new Command('deploy')
|
|
|
101
127
|
}
|
|
102
128
|
const params = errorResult.retryParams || errorResult;
|
|
103
129
|
Object.assign(extraParams, params);
|
|
104
|
-
|
|
105
|
-
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
130
|
+
result = await submit();
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
formatResponse(result, { json: options.json });
|
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) {
|
|
@@ -85,10 +90,12 @@ export const deployCommand = new Command('deploy')
|
|
|
85
90
|
continue;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
const
|
|
89
|
-
const
|
|
93
|
+
const isMultipart = entry.multipart === true;
|
|
94
|
+
const entity = entry.entity || (isMultipart ? 'media' : 'content');
|
|
95
|
+
const column = entry.column || (isMultipart ? 'File' : 'Content');
|
|
90
96
|
const uid = entry.uid;
|
|
91
97
|
const file = entry.file;
|
|
98
|
+
const filename = entry.filename || file.split('/').pop();
|
|
92
99
|
|
|
93
100
|
if (!uid || !file) {
|
|
94
101
|
log.warn(`Skipping "${entryName}": missing uid or file.`);
|
|
@@ -101,7 +108,10 @@ export const deployCommand = new Command('deploy')
|
|
|
101
108
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
102
109
|
if (activeModifyKey) extraParams['_modify_key'] = activeModifyKey;
|
|
103
110
|
|
|
104
|
-
const
|
|
111
|
+
const rowKeyExpr = transactionKey === 'RowID' ? `RowID:${uid}` : `RowUID:${uid}`;
|
|
112
|
+
const dataExprs = isMultipart
|
|
113
|
+
? [`${rowKeyExpr};column:${entity}.Filename=${filename}`]
|
|
114
|
+
: [`${rowKeyExpr};column:${entity}.${column}@${file}`];
|
|
105
115
|
|
|
106
116
|
// Apply stored ticket if no --ticket flag
|
|
107
117
|
if (!options.ticket) {
|
|
@@ -112,8 +122,29 @@ export const deployCommand = new Command('deploy')
|
|
|
112
122
|
}
|
|
113
123
|
await applyStoredTicketToSubmission(dataExprs, entity, uid, uid, options, sessionTicketOverride);
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
// Submit helper — handles both URL-encoded and multipart modes
|
|
126
|
+
async function submit() {
|
|
127
|
+
if (isMultipart) {
|
|
128
|
+
const fields = { ...extraParams };
|
|
129
|
+
for (const expr of dataExprs) {
|
|
130
|
+
const eqIdx = expr.indexOf('=');
|
|
131
|
+
if (eqIdx !== -1) {
|
|
132
|
+
fields[expr.substring(0, eqIdx)] = expr.substring(eqIdx + 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const files = [{
|
|
136
|
+
fieldName: `${rowKeyExpr};column:${entity}.${column}`,
|
|
137
|
+
filePath: file,
|
|
138
|
+
fileName: file.split('/').pop(),
|
|
139
|
+
}];
|
|
140
|
+
return client.postMultipart('/api/input/submit', fields, files);
|
|
141
|
+
} else {
|
|
142
|
+
const body = await buildInputBody(dataExprs, extraParams);
|
|
143
|
+
return client.postUrlEncoded('/api/input/submit', body);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let result = await submit();
|
|
117
148
|
|
|
118
149
|
// Reactive ModifyKey retry
|
|
119
150
|
if (!result.successful && result.messages?.some(m => isModifyKeyError(m))) {
|
|
@@ -121,8 +152,7 @@ export const deployCommand = new Command('deploy')
|
|
|
121
152
|
if (retryMK.cancel) { log.info('Submission cancelled'); break; }
|
|
122
153
|
activeModifyKey = retryMK.modifyKey;
|
|
123
154
|
extraParams['_modify_key'] = activeModifyKey;
|
|
124
|
-
|
|
125
|
-
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
155
|
+
result = await submit();
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
@@ -141,8 +171,7 @@ export const deployCommand = new Command('deploy')
|
|
|
141
171
|
}
|
|
142
172
|
const params = errorResult.retryParams || errorResult;
|
|
143
173
|
Object.assign(extraParams, params);
|
|
144
|
-
|
|
145
|
-
result = await client.postUrlEncoded('/api/input/submit', body);
|
|
174
|
+
result = await submit();
|
|
146
175
|
}
|
|
147
176
|
|
|
148
177
|
if (result.successful) {
|
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
|
+
}
|