primitive-admin 1.0.50 → 1.0.51
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 +27 -8
- package/dist/src/commands/admins.js +25 -27
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/apps.js +8 -0
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +30 -26
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +17 -18
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +9 -9
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +33 -36
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/database-types.js +17 -18
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.js +41 -45
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +17 -18
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +9 -9
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +9 -9
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +17 -18
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/integrations.js +17 -18
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.js +4 -2
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/prompts.js +33 -36
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +9 -9
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/sync.d.ts +0 -14
- package/dist/src/commands/sync.js +143 -91
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +9 -9
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +41 -45
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/waitlist.js +9 -9
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.js +9 -9
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +62 -36
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +9 -24
- package/dist/src/lib/api-client.js +36 -33
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +25 -8
- package/dist/src/lib/confirm-prompt.js +44 -19
- package/dist/src/lib/confirm-prompt.js.map +1 -1
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +181 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +26 -17
- package/dist/src/lib/workflow-toml-validator.js +52 -141
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/package.json +4 -1
|
@@ -12,6 +12,7 @@ import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneS
|
|
|
12
12
|
import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
|
|
13
13
|
import { expandWorkflowTomlData } from "../lib/workflow-fragments.js";
|
|
14
14
|
import { success, error, info, warn, keyValue, json, divider, } from "../lib/output.js";
|
|
15
|
+
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
15
16
|
import chalk from "chalk";
|
|
16
17
|
function ensureDir(dirPath) {
|
|
17
18
|
if (!existsSync(dirPath)) {
|
|
@@ -260,7 +261,13 @@ function serializeBlobBucket(bucket) {
|
|
|
260
261
|
name: bucket.name,
|
|
261
262
|
description: bucket.description || undefined,
|
|
262
263
|
ttlTier: bucket.ttlTier,
|
|
263
|
-
|
|
264
|
+
// #1020: `preset` is the honest key; a custom bucket carries ruleSetId
|
|
265
|
+
// instead. Omit preset for custom buckets (preset === "custom").
|
|
266
|
+
preset: bucket.preset && bucket.preset !== "custom"
|
|
267
|
+
? bucket.preset
|
|
268
|
+
: bucket.ruleSetId
|
|
269
|
+
? undefined
|
|
270
|
+
: bucket.preset,
|
|
264
271
|
},
|
|
265
272
|
};
|
|
266
273
|
if (bucket.ruleSetId) {
|
|
@@ -313,6 +320,9 @@ function serializeWorkflow(workflow, draft, configs) {
|
|
|
313
320
|
status: workflow.status,
|
|
314
321
|
activeConfigName: activeConfigName,
|
|
315
322
|
accessRule: workflow.accessRule || undefined,
|
|
323
|
+
// #1081 — workflow principal mode. Emit only when set so a fresh
|
|
324
|
+
// pull → push round-trips it without writing a noisy `runAs = ""`.
|
|
325
|
+
runAs: workflow.runAs || undefined,
|
|
316
326
|
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
317
327
|
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
318
328
|
dequeueOrder: workflow.dequeueOrder,
|
|
@@ -734,56 +744,6 @@ export async function pullScripts(client, appId, configDir, logger = () => { })
|
|
|
734
744
|
}
|
|
735
745
|
return { scriptEntities, count: items.length };
|
|
736
746
|
}
|
|
737
|
-
/**
|
|
738
|
-
* Issue #973 — format the "stale referencing workflows" warning for a script
|
|
739
|
-
* push. Pure (no I/O) so it can be unit-tested directly.
|
|
740
|
-
*
|
|
741
|
-
* `staleWorkflows` is the server's report of active workflows whose frozen
|
|
742
|
-
* snapshot still pins a DIFFERENT (old) body hash for `scriptName` — i.e. the
|
|
743
|
-
* workflows that will keep running the previous script body until republished.
|
|
744
|
-
* Returns `null` when nothing is stale (no warning to print). When `dryRun` is
|
|
745
|
-
* true the wording reflects that nothing was mutated yet.
|
|
746
|
-
*/
|
|
747
|
-
export function formatStaleWorkflowsWarning(scriptName, staleWorkflows, dryRun = false) {
|
|
748
|
-
if (!staleWorkflows || staleWorkflows.length === 0)
|
|
749
|
-
return null;
|
|
750
|
-
const labels = staleWorkflows.map((w) => w.workflowKey || w.name || "(unknown workflow)");
|
|
751
|
-
const count = labels.length;
|
|
752
|
-
const noun = count === 1 ? "workflow" : "workflows";
|
|
753
|
-
const verb = dryRun ? "would still run" : "still run";
|
|
754
|
-
return (`Script "${scriptName}" ${dryRun ? "would be updated" : "updated"}, but ` +
|
|
755
|
-
`${count} ${noun} ${verb} the previous body until re-pushed: ` +
|
|
756
|
-
`${labels.join(", ")}. ` +
|
|
757
|
-
`Re-push (or republish) ${count === 1 ? "it" : "them"} to pick up the change ` +
|
|
758
|
-
`(the run path reads each workflow's frozen snapshot, not the live script).`);
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Issue #973 — query the server for active workflows left stale by a script
|
|
762
|
-
* push and print a warning naming them. Best-effort: a missing route (older
|
|
763
|
-
* server) or any transport error is swallowed so it never fails the sync.
|
|
764
|
-
*
|
|
765
|
-
* `contentHash` is the body hash being pushed (the server's authoritative hash
|
|
766
|
-
* after a real push, or the locally-computed would-be hash for `--dry-run`).
|
|
767
|
-
*/
|
|
768
|
-
async function warnStaleWorkflowsForScript(client, appId, scriptId, contentHash, dryRun) {
|
|
769
|
-
let report;
|
|
770
|
-
try {
|
|
771
|
-
report = await client.getStaleWorkflowsForScript(appId, scriptId, contentHash);
|
|
772
|
-
}
|
|
773
|
-
catch {
|
|
774
|
-
// Older server without the route, or a transient failure — staleness
|
|
775
|
-
// info is advisory, so we stay silent rather than break the push.
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const message = formatStaleWorkflowsWarning(report.scriptName, report.staleWorkflows, dryRun);
|
|
779
|
-
if (message) {
|
|
780
|
-
warn(` ${message}`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/** SHA-256 hex of a string body, matching the server's script `contentHash`. */
|
|
784
|
-
function sha256HexString(body) {
|
|
785
|
-
return createHash("sha256").update(body, "utf-8").digest("hex");
|
|
786
|
-
}
|
|
787
747
|
async function pullTestCasesForBlock(client, appId, blockType, blockId, blockKey, configDir, testCaseEntities, lookupMaps) {
|
|
788
748
|
let testCases;
|
|
789
749
|
try {
|
|
@@ -2273,8 +2233,29 @@ Directory Structure:
|
|
|
2273
2233
|
continue;
|
|
2274
2234
|
}
|
|
2275
2235
|
if (existingId) {
|
|
2276
|
-
//
|
|
2277
|
-
|
|
2236
|
+
// #1020 (D8): idempotently apply preset/ruleSet/name/description
|
|
2237
|
+
// changes via PATCH. bucketKey/ttlTier are immutable and not sent.
|
|
2238
|
+
const updatePayload = {};
|
|
2239
|
+
if (bucket.preset)
|
|
2240
|
+
updatePayload.preset = bucket.preset;
|
|
2241
|
+
else if (bucket.accessPolicy)
|
|
2242
|
+
updatePayload.accessPolicy = bucket.accessPolicy;
|
|
2243
|
+
if (bucket.ruleSetId)
|
|
2244
|
+
updatePayload.ruleSetId = bucket.ruleSetId;
|
|
2245
|
+
if (bucket.name)
|
|
2246
|
+
updatePayload.name = bucket.name;
|
|
2247
|
+
if (bucket.description !== undefined)
|
|
2248
|
+
updatePayload.description = bucket.description || null;
|
|
2249
|
+
changes.push({ type: "blob-bucket", action: "update", key });
|
|
2250
|
+
if (!options.dryRun) {
|
|
2251
|
+
try {
|
|
2252
|
+
await client.updateBlobBucket(resolvedAppId, existingId, updatePayload);
|
|
2253
|
+
info(` Updated blob bucket: ${key}`);
|
|
2254
|
+
}
|
|
2255
|
+
catch (err) {
|
|
2256
|
+
warn(` Failed to update blob bucket ${key}: ${String(err?.message || err)}`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2278
2259
|
if (syncState?.entities?.blobBuckets?.[key]) {
|
|
2279
2260
|
syncState.entities.blobBuckets[key].contentHash = computeFileHash(filePath);
|
|
2280
2261
|
}
|
|
@@ -2284,8 +2265,11 @@ Directory Structure:
|
|
|
2284
2265
|
bucketKey: key,
|
|
2285
2266
|
name: bucket.name || key,
|
|
2286
2267
|
ttlTier: bucket.ttlTier,
|
|
2287
|
-
accessPolicy: bucket.accessPolicy,
|
|
2288
2268
|
};
|
|
2269
|
+
if (bucket.preset)
|
|
2270
|
+
payload.preset = bucket.preset;
|
|
2271
|
+
else if (bucket.accessPolicy)
|
|
2272
|
+
payload.accessPolicy = bucket.accessPolicy;
|
|
2289
2273
|
if (bucket.description)
|
|
2290
2274
|
payload.description = bucket.description;
|
|
2291
2275
|
if (bucket.ruleSetId)
|
|
@@ -2578,7 +2562,10 @@ Directory Structure:
|
|
|
2578
2562
|
changes.push({ type: "script", action: "update", key: name });
|
|
2579
2563
|
if (!options.dryRun) {
|
|
2580
2564
|
try {
|
|
2581
|
-
|
|
2565
|
+
// #1000 — push a new ScriptConfig + activate it. Referencing
|
|
2566
|
+
// workflows pick up the new body on their next run (no
|
|
2567
|
+
// fan-out, no re-push needed).
|
|
2568
|
+
await client.pushScriptBody(resolvedAppId, existingId, body);
|
|
2582
2569
|
info(` Updated script: ${name}`);
|
|
2583
2570
|
if (syncState) {
|
|
2584
2571
|
if (!syncState.entities.scripts) {
|
|
@@ -2586,24 +2573,16 @@ Directory Structure:
|
|
|
2586
2573
|
}
|
|
2587
2574
|
syncState.entities.scripts[name] = {
|
|
2588
2575
|
id: existingId,
|
|
2589
|
-
modifiedAt:
|
|
2590
|
-
syncState.entities.scripts[name]?.modifiedAt ||
|
|
2576
|
+
modifiedAt: syncState.entities.scripts[name]?.modifiedAt ||
|
|
2591
2577
|
new Date().toISOString(),
|
|
2592
2578
|
contentHash: computeFileHash(filePath),
|
|
2593
2579
|
};
|
|
2594
2580
|
}
|
|
2595
|
-
// Issue #973 — warn if active workflows still pin the OLD
|
|
2596
|
-
// body. Use the server's authoritative post-push hash.
|
|
2597
|
-
await warnStaleWorkflowsForScript(client, resolvedAppId, existingId, updated?.contentHash || sha256HexString(body), false);
|
|
2598
2581
|
}
|
|
2599
2582
|
catch (err) {
|
|
2600
2583
|
throw wrapEntityError(err, "update", "script", name);
|
|
2601
2584
|
}
|
|
2602
2585
|
}
|
|
2603
|
-
else {
|
|
2604
|
-
// Dry-run: surface the would-be stale set without mutating.
|
|
2605
|
-
await warnStaleWorkflowsForScript(client, resolvedAppId, existingId, sha256HexString(body), true);
|
|
2606
|
-
}
|
|
2607
2586
|
}
|
|
2608
2587
|
else {
|
|
2609
2588
|
// No id in local state — could be brand new OR could be
|
|
@@ -2616,7 +2595,7 @@ Directory Structure:
|
|
|
2616
2595
|
changes.push({ type: "script", action: "update", key: name });
|
|
2617
2596
|
if (!options.dryRun) {
|
|
2618
2597
|
try {
|
|
2619
|
-
|
|
2598
|
+
await client.pushScriptBody(resolvedAppId, match.scriptId, body);
|
|
2620
2599
|
info(` Updated script: ${name} (adopted from server)`);
|
|
2621
2600
|
if (syncState) {
|
|
2622
2601
|
if (!syncState.entities.scripts) {
|
|
@@ -2624,21 +2603,15 @@ Directory Structure:
|
|
|
2624
2603
|
}
|
|
2625
2604
|
syncState.entities.scripts[name] = {
|
|
2626
2605
|
id: match.scriptId,
|
|
2627
|
-
modifiedAt:
|
|
2606
|
+
modifiedAt: new Date().toISOString(),
|
|
2628
2607
|
contentHash: computeFileHash(filePath),
|
|
2629
2608
|
};
|
|
2630
2609
|
}
|
|
2631
|
-
// Issue #973 — same staleness warning on the adopt path.
|
|
2632
|
-
await warnStaleWorkflowsForScript(client, resolvedAppId, match.scriptId, updated?.contentHash || sha256HexString(body), false);
|
|
2633
2610
|
}
|
|
2634
2611
|
catch (err) {
|
|
2635
2612
|
throw wrapEntityError(err, "update", "script", name);
|
|
2636
2613
|
}
|
|
2637
2614
|
}
|
|
2638
|
-
else {
|
|
2639
|
-
// Dry-run on the adopt path.
|
|
2640
|
-
await warnStaleWorkflowsForScript(client, resolvedAppId, match.scriptId, sha256HexString(body), true);
|
|
2641
|
-
}
|
|
2642
2615
|
}
|
|
2643
2616
|
else {
|
|
2644
2617
|
changes.push({ type: "script", action: "create", key: name });
|
|
@@ -2738,6 +2711,10 @@ Directory Structure:
|
|
|
2738
2711
|
description: workflow.description,
|
|
2739
2712
|
status: workflow.status,
|
|
2740
2713
|
accessRule: workflow.accessRule !== undefined ? (workflow.accessRule || null) : undefined,
|
|
2714
|
+
// #1081 — workflow principal mode. Absent key → undefined
|
|
2715
|
+
// (server leaves it untouched); explicit value/empty is
|
|
2716
|
+
// forwarded so it round-trips.
|
|
2717
|
+
runAs: workflow.runAs !== undefined ? (workflow.runAs || null) : undefined,
|
|
2741
2718
|
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
2742
2719
|
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
2743
2720
|
dequeueOrder: workflow.dequeueOrder,
|
|
@@ -2832,6 +2809,8 @@ Directory Structure:
|
|
|
2832
2809
|
inputSchema: workflow.inputSchema ? safeJsonParse(workflow.inputSchema) : undefined,
|
|
2833
2810
|
outputSchema: workflow.outputSchema ? safeJsonParse(workflow.outputSchema) : undefined,
|
|
2834
2811
|
accessRule: workflow.accessRule || undefined,
|
|
2812
|
+
// #1081 — workflow principal mode (absent → server default).
|
|
2813
|
+
runAs: workflow.runAs || undefined,
|
|
2835
2814
|
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
2836
2815
|
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
2837
2816
|
dequeueOrder: workflow.dequeueOrder,
|
|
@@ -2946,6 +2925,15 @@ Directory Structure:
|
|
|
2946
2925
|
`\nAborting push: ${validation.errors.length} unresolved $params reference(s) in ${file}.`);
|
|
2947
2926
|
}
|
|
2948
2927
|
const existingEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
2928
|
+
// Issue #915 (defect b): track whether the apply path rejected any
|
|
2929
|
+
// op for THIS file via the schema gate (OperationRefError /
|
|
2930
|
+
// SchemaRequiredError). On a non-transactional fresh-type push the
|
|
2931
|
+
// db-type lands but the bad op is blocked and the loop continues; if
|
|
2932
|
+
// we then wrote the file-level contentHash, the next push's
|
|
2933
|
+
// `shouldPushFile` would short-circuit and the missing op would be
|
|
2934
|
+
// invisible drift. When this flag is set we skip the contentHash
|
|
2935
|
+
// write so the drift stays visible and a corrected re-push converges.
|
|
2936
|
+
let fileHadGateRejectedOp = false;
|
|
2949
2937
|
// Skip if file hasn't changed since last sync
|
|
2950
2938
|
if (!options.force && existingEntry && !shouldPushFile(filePath, existingEntry.contentHash)) {
|
|
2951
2939
|
skippedCount++;
|
|
@@ -3032,8 +3020,7 @@ Directory Structure:
|
|
|
3032
3020
|
}
|
|
3033
3021
|
// 2. Op-edit gate (op create/update dry-run) for every op in the
|
|
3034
3022
|
// TOML. The server runs `runSchemaGate` and short-circuits
|
|
3035
|
-
// before persisting.
|
|
3036
|
-
// server yet → gate is a no-op anyway).
|
|
3023
|
+
// before persisting.
|
|
3037
3024
|
//
|
|
3038
3025
|
// The op gate is run against the schema THIS push is landing
|
|
3039
3026
|
// (`schemaOverride`), not the stale stored schema — the real
|
|
@@ -3043,11 +3030,36 @@ Directory Structure:
|
|
|
3043
3030
|
// a schema landed earlier in the same push"). When the push
|
|
3044
3031
|
// isn't changing the schema, leave the override unset so the
|
|
3045
3032
|
// server uses the stored schema.
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3033
|
+
//
|
|
3034
|
+
// Issue #915 (defect a): this gate now runs for FRESH types too
|
|
3035
|
+
// (previously guarded by `if (existingEntry)`). For a brand-new
|
|
3036
|
+
// db-type the server has no stored config, so we gate the op
|
|
3037
|
+
// against the PROPOSED local schema via `schemaOverride`. The
|
|
3038
|
+
// server's op-create dry-run accepts a missing type when a
|
|
3039
|
+
// `schemaOverride` is present (see
|
|
3040
|
+
// database-type-operations-controller.ts). A fresh schemaless
|
|
3041
|
+
// type (#666 bootstrap) sends an empty/absent override, and
|
|
3042
|
+
// `runSchemaGate` is a no-op without a schema — so that path
|
|
3043
|
+
// still succeeds.
|
|
3044
|
+
{
|
|
3045
|
+
const localHasSchema = typeof typeConfig.schema === "string" &&
|
|
3046
|
+
typeConfig.schema.trim().length > 0;
|
|
3047
|
+
let proposedSchema;
|
|
3048
|
+
if (existingEntry) {
|
|
3049
|
+
const validateUpdateData = computeTypeUpdateData();
|
|
3050
|
+
proposedSchema =
|
|
3051
|
+
"schema" in validateUpdateData
|
|
3052
|
+
? validateUpdateData.schema
|
|
3053
|
+
: undefined;
|
|
3054
|
+
}
|
|
3055
|
+
else {
|
|
3056
|
+
// Fresh type: the proposed schema is whatever the local TOML
|
|
3057
|
+
// declares. A schemaless fresh type (no `[models.*]`) sends an
|
|
3058
|
+
// empty-string override so the server takes the no-schema =
|
|
3059
|
+
// no-op gate path rather than falling back to a (nonexistent)
|
|
3060
|
+
// stored config — keeping the #666 bootstrap convergent.
|
|
3061
|
+
proposedSchema = localHasSchema ? typeConfig.schema : "";
|
|
3062
|
+
}
|
|
3051
3063
|
for (const op of operations) {
|
|
3052
3064
|
const existingOp = existingOpsForType[op.name];
|
|
3053
3065
|
opGateContext = {
|
|
@@ -3071,7 +3083,20 @@ Directory Structure:
|
|
|
3071
3083
|
access: op.access,
|
|
3072
3084
|
definition: op.definition,
|
|
3073
3085
|
params: op.params,
|
|
3074
|
-
}, {
|
|
3086
|
+
}, {
|
|
3087
|
+
dryRun: true,
|
|
3088
|
+
schemaOverride: proposedSchema,
|
|
3089
|
+
// Issue #915 (defect a, follow-up): for a fresh type the
|
|
3090
|
+
// server has no stored config to read `defaultAccess`
|
|
3091
|
+
// from, so an op that omits `access` to inherit the
|
|
3092
|
+
// TOML-declared type-level `defaultAccess` would be
|
|
3093
|
+
// falsely rejected. Thread the proposed value so the
|
|
3094
|
+
// fresh-type dry-run gates the same way the real push
|
|
3095
|
+
// (which lands the type first) will.
|
|
3096
|
+
defaultAccess: typeof typeConfig.defaultAccess === "string"
|
|
3097
|
+
? typeConfig.defaultAccess
|
|
3098
|
+
: undefined,
|
|
3099
|
+
});
|
|
3075
3100
|
}
|
|
3076
3101
|
}
|
|
3077
3102
|
// Every op gate passed; clear the context so a later (non-op)
|
|
@@ -3563,6 +3588,10 @@ Directory Structure:
|
|
|
3563
3588
|
key: `${dbType}/${op.name}`,
|
|
3564
3589
|
message: err.message,
|
|
3565
3590
|
});
|
|
3591
|
+
// Issue #915 (defect b): a gate-rejected op means this
|
|
3592
|
+
// file's state did NOT fully land — don't poison the
|
|
3593
|
+
// content hash (see the contentHash write below).
|
|
3594
|
+
fileHadGateRejectedOp = true;
|
|
3566
3595
|
}
|
|
3567
3596
|
else if (err instanceof OperationRefError) {
|
|
3568
3597
|
schemaErrors.opRefs.push({
|
|
@@ -3571,6 +3600,7 @@ Directory Structure:
|
|
|
3571
3600
|
refs: err.refs,
|
|
3572
3601
|
message: err.message,
|
|
3573
3602
|
});
|
|
3603
|
+
fileHadGateRejectedOp = true;
|
|
3574
3604
|
}
|
|
3575
3605
|
else {
|
|
3576
3606
|
throw wrapEntityError(err, "update", "operation", `${dbType}/${op.name}`);
|
|
@@ -3608,6 +3638,10 @@ Directory Structure:
|
|
|
3608
3638
|
key: `${dbType}/${op.name}`,
|
|
3609
3639
|
message: err.message,
|
|
3610
3640
|
});
|
|
3641
|
+
// Issue #915 (defect b): a gate-rejected op means this
|
|
3642
|
+
// file's state did NOT fully land — don't poison the
|
|
3643
|
+
// content hash (see the contentHash write below).
|
|
3644
|
+
fileHadGateRejectedOp = true;
|
|
3611
3645
|
}
|
|
3612
3646
|
else if (err instanceof OperationRefError) {
|
|
3613
3647
|
schemaErrors.opRefs.push({
|
|
@@ -3616,6 +3650,7 @@ Directory Structure:
|
|
|
3616
3650
|
refs: err.refs,
|
|
3617
3651
|
message: err.message,
|
|
3618
3652
|
});
|
|
3653
|
+
fileHadGateRejectedOp = true;
|
|
3619
3654
|
}
|
|
3620
3655
|
else {
|
|
3621
3656
|
throw wrapEntityError(err, "create", "operation", `${dbType}/${op.name}`);
|
|
@@ -3747,9 +3782,27 @@ Directory Structure:
|
|
|
3747
3782
|
// file. Recompute here so a truly-unchanged subsequent push skips
|
|
3748
3783
|
// the whole file via `shouldPushFile`. `--force` bypasses that
|
|
3749
3784
|
// skip, so forced re-pushes are unaffected.
|
|
3785
|
+
//
|
|
3786
|
+
// Issue #915 (defect b): when the gate rejected an op for this
|
|
3787
|
+
// file, do NOT record a matching content hash. The db-type itself
|
|
3788
|
+
// may have been created (the push is non-transactional), but the
|
|
3789
|
+
// blocked op never landed — a matching hash would make
|
|
3790
|
+
// `shouldPushFile` short-circuit the next push and hide the missing
|
|
3791
|
+
// op as "in sync". Clear the hash instead of writing it: an empty
|
|
3792
|
+
// hash forces `shouldPushFile` to re-evaluate the file, so a
|
|
3793
|
+
// subsequent `sync push --dry-run` still surfaces the op as drift
|
|
3794
|
+
// and a corrected re-push converges without manual file edits. We
|
|
3795
|
+
// clear (rather than merely skip) so the fresh-type create path's
|
|
3796
|
+
// earlier optimistic hash write (see `createDatabaseTypeConfig`
|
|
3797
|
+
// above) is also invalidated.
|
|
3750
3798
|
if (!options.dryRun && syncState?.entities?.databaseTypes?.[dbType]) {
|
|
3751
|
-
|
|
3752
|
-
|
|
3799
|
+
if (fileHadGateRejectedOp) {
|
|
3800
|
+
syncState.entities.databaseTypes[dbType].contentHash = "";
|
|
3801
|
+
}
|
|
3802
|
+
else {
|
|
3803
|
+
syncState.entities.databaseTypes[dbType].contentHash =
|
|
3804
|
+
computeFileHash(filePath);
|
|
3805
|
+
}
|
|
3753
3806
|
}
|
|
3754
3807
|
}
|
|
3755
3808
|
}
|
|
@@ -4572,15 +4625,14 @@ Directory Structure:
|
|
|
4572
4625
|
warn(`You have uncommitted git changes under ${configDir}. ` +
|
|
4573
4626
|
"Reverting will overwrite them.");
|
|
4574
4627
|
}
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
{
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
]);
|
|
4628
|
+
let confirm;
|
|
4629
|
+
try {
|
|
4630
|
+
confirm = await confirmPrompt(`Restore snapshot ${target.id} into ${configDir}? This overwrites the current sync directory.`);
|
|
4631
|
+
}
|
|
4632
|
+
catch (err) {
|
|
4633
|
+
error(err.message);
|
|
4634
|
+
process.exit(1);
|
|
4635
|
+
}
|
|
4584
4636
|
if (!confirm) {
|
|
4585
4637
|
info("Cancelled.");
|
|
4586
4638
|
return;
|