primitive-admin 1.2.0-alpha.11 → 1.2.0-alpha.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/dist/src/commands/agent-sessions.d.ts +26 -10
- package/dist/src/commands/agent-sessions.js +80 -83
- package/dist/src/commands/agent-sessions.js.map +1 -1
- package/dist/src/commands/init.js +108 -34
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/locks.d.ts +5 -3
- package/dist/src/commands/locks.js +17 -61
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -1
- package/dist/src/commands/rule-sets.js +0 -1
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/sync.d.ts +28 -0
- package/dist/src/commands/sync.js +255 -70
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +6 -4
- package/dist/src/lib/api-client.js +4 -14
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +1 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +13 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +13 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
- package/dist/src/lib/db3-type-models.d.ts +92 -0
- package/dist/src/lib/db3-type-models.js +321 -0
- package/dist/src/lib/db3-type-models.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +3 -1
- package/dist/src/lib/function-db-types.js +42 -8
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-typecheck.js +3 -0
- package/dist/src/lib/function-typecheck.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +19 -2
- package/dist/src/lib/generated-config-surfaces.js +38 -3
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/init-assets.d.ts +2 -6
- package/dist/src/lib/init-assets.js +11 -27
- package/dist/src/lib/init-assets.js.map +1 -1
- package/dist/src/lib/init-ios-links.d.ts +11 -7
- package/dist/src/lib/init-ios-links.js +28 -22
- package/dist/src/lib/init-ios-links.js.map +1 -1
- package/dist/src/lib/init-schema.d.ts +15 -0
- package/dist/src/lib/init-schema.js +54 -0
- package/dist/src/lib/init-schema.js.map +1 -1
- package/dist/src/lib/session-scope.d.ts +26 -0
- package/dist/src/lib/session-scope.js +58 -0
- package/dist/src/lib/session-scope.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +54 -0
- package/dist/src/lib/sync-resource-types.js +92 -1
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- package/dist/src/lib/template.d.ts +39 -3
- package/dist/src/lib/template.js +103 -14
- package/dist/src/lib/template.js.map +1 -1
- package/package.json +2 -2
|
@@ -9,7 +9,8 @@ import { buildDatabaseTypeTomlData } from "../lib/toml-database-config.js";
|
|
|
9
9
|
// #3317 — see `config-json-field.ts`.
|
|
10
10
|
import { serializeJsonField } from "../lib/config-json-field.js";
|
|
11
11
|
import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMetadataCategoryConfig, parseDeclaredAccessManifestToml, validateDeclaredAccessManifest, } from "../lib/toml-metadata-config.js";
|
|
12
|
-
import { getCurrentAppId, getServerUrl } from "../lib/config.js";
|
|
12
|
+
import { getCurrentAppId, getServerUrl, loadCredentials } from "../lib/config.js";
|
|
13
|
+
import { sessionHoldsScope, tokenReachesScope, } from "../lib/session-scope.js";
|
|
13
14
|
import { requireCurrentEnvironment } from "../lib/env-resolver.js";
|
|
14
15
|
import { ensureFileIgnored } from "../lib/local-state.js";
|
|
15
16
|
import { PROJECT_TREE_DIR } from "../lib/env-resolver-core.js";
|
|
@@ -17,6 +18,7 @@ import { cachedVarsNote, layerVars, planVarsOverridePull, varsOverridePath as re
|
|
|
17
18
|
import { resolveSyncDir, resolveSnapshotsRoot, resolveSyncStateReadPath, resolveSyncStateWritePath, SYNC_STATE_FILENAME, checkLegacyDatabaseTypesDir, } from "../lib/sync-paths.js";
|
|
18
19
|
import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneSnapshots, } from "../lib/snapshots.js";
|
|
19
20
|
import { parseConfigToml, stringifyConfigToml } from "../lib/config-toml.js";
|
|
21
|
+
import { cloneTomlValue, planDb3TypePull, readDb3TypesDir, resolveDb3TypeModels } from "../lib/db3-type-models.js";
|
|
20
22
|
import { canonicalizeIntegrationRequestConfig } from "../lib/integration-request-config.js";
|
|
21
23
|
import { canonicalizeServerTextFields } from "../lib/server-text-normalization.js";
|
|
22
24
|
import { canonicalJsonStringify } from "../lib/canonical-json.js";
|
|
@@ -27,6 +29,9 @@ import { collectAppSettingsPushErrors, writePulledAppSettings, diffAppSettings,
|
|
|
27
29
|
import { BLOB_BUCKET_TABLE, DATABASE_TYPE_TABLE, DB3_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, configDeclaredTypeErrors, deprecatedConfigKey, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownTomlKeys, misshapenTableMessage, retiredTomlKey, retiredDocumentKey, spellsDecimalNumber, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
28
30
|
import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
|
|
29
31
|
import { CONFIG_SURFACES, isEmptyForEmit, tomlGroupsOf, RETIRED_CONFIG_DIRECTORIES, RETIRED_TYPE_CONFIG_DIRECTORIES, } from "../lib/generated-config-surfaces.js";
|
|
32
|
+
// #4959 — the retired rule-set resource types, vendored with the one sentence
|
|
33
|
+
// the rule-set routes answer, so the preflight refuses exactly what they do.
|
|
34
|
+
import { retiredRuleSetResourceType } from "../lib/generated-config-surfaces.js";
|
|
30
35
|
// #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
|
|
31
36
|
// preflight refuses exactly what the admin routes refuse.
|
|
32
37
|
import { promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
|
|
@@ -66,7 +71,7 @@ import { isFunctionDeclarationFile, resolveDocumentSchema, } from "../lib/functi
|
|
|
66
71
|
import { createFunctionTypechecker, } from "../lib/function-typecheck.js";
|
|
67
72
|
import { resolveFunctionSourceRoots } from "../lib/function-source-roots.js";
|
|
68
73
|
import { detectLayout, migrateLegacyToV2 } from "../lib/block-layout.js";
|
|
69
|
-
import { ABSENT_FROM_EXPORT, LOCAL_ONLY_ABSENT, LOCAL_ONLY_NEW, PUSH_PRUNE_SPECS, REMOTE_ONLY_UNMANAGED, REMOTE_ONLY_WILL_DELETE, SYNC_RESOURCE_TYPES, VALIDATION_ERROR, applyPrune, attachmentWritePath, attemptFetch, classifyLocalOnly, classifyPruneDeleteFailure, classifyRemoteOnly,
|
|
74
|
+
import { ABSENT_FROM_EXPORT, LOCAL_ONLY_ABSENT, LOCAL_ONLY_NEW, PUSH_PRUNE_SPECS, REMOTE_ONLY_UNMANAGED, REMOTE_ONLY_WILL_DELETE, SYNC_RESOURCE_TYPES, VALIDATION_ERROR, applyPrune, attachmentWritePath, attemptFetch, classifyLocalOnly, classifyPruneDeleteFailure, classifyRemoteOnly, formatPrunePlan, hasUncommittedChanges, isSafeAttachmentFilename, planPrune, planPushPrune, preserveUnwrittenEntries, pruneAdminScopeRefusal, pruneScopeUnknownRefusal, removePrunedSidecar, ruleSetFileKey, } from "../lib/sync-resource-types.js";
|
|
70
75
|
import { normalizeTestCaseKey, testCaseKeyError, } from "../lib/test-case-keys.js";
|
|
71
76
|
import { slugifyTestCaseName, testCaseFileBasenames } from "../lib/test-case-file-names.js";
|
|
72
77
|
// The sidecar naming rules live in `lib/` (#3061); re-exported here because
|
|
@@ -1459,6 +1464,37 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
1459
1464
|
* spelling with no scheduled removal (a function's frozen `access`, #4738) is
|
|
1460
1465
|
* not one of them. A repeated table's entries are read one by one.
|
|
1461
1466
|
*/
|
|
1467
|
+
/**
|
|
1468
|
+
* #4989 — whether this session can make the prune's hard deletes in `appId`:
|
|
1469
|
+
* read from the stored token and step-up where they say, with the app's
|
|
1470
|
+
* parent looked up only when a pin on it could be what reaches the app, and
|
|
1471
|
+
* from the server for an opaque CI token. Rejects when a read it needs fails;
|
|
1472
|
+
* the caller treats that as unknown, never as `admin`.
|
|
1473
|
+
*/
|
|
1474
|
+
async function sessionHoldsAdminFor(client, appId) {
|
|
1475
|
+
const credentials = loadCredentials();
|
|
1476
|
+
// No stored session to read: the server judges each delete.
|
|
1477
|
+
if (!credentials)
|
|
1478
|
+
return true;
|
|
1479
|
+
const target = { appId };
|
|
1480
|
+
let holds = sessionHoldsScope(credentials, "admin", target);
|
|
1481
|
+
if (holds === true)
|
|
1482
|
+
return true;
|
|
1483
|
+
const app = (await client.getApp(appId));
|
|
1484
|
+
target.parentAppId = app?.parentAppId ?? null;
|
|
1485
|
+
holds = sessionHoldsScope(credentials, "admin", target);
|
|
1486
|
+
if (holds !== null)
|
|
1487
|
+
return holds;
|
|
1488
|
+
// An opaque CI token: the server says what it holds. "full" is unscoped.
|
|
1489
|
+
const me = (await client.getMe());
|
|
1490
|
+
const session = me?.session;
|
|
1491
|
+
if (!session)
|
|
1492
|
+
return true;
|
|
1493
|
+
return tokenReachesScope({
|
|
1494
|
+
scope: Array.isArray(session.scope) ? session.scope : undefined,
|
|
1495
|
+
apps: session.apps,
|
|
1496
|
+
}, "admin", target);
|
|
1497
|
+
}
|
|
1462
1498
|
function deprecatedKeysPresent(tomlData, table) {
|
|
1463
1499
|
const prefix = table.tomlPath.join(".");
|
|
1464
1500
|
const at = table.tomlPath.reduce((node, segment) => (node == null ? node : node[segment]), tomlData);
|
|
@@ -1680,6 +1716,31 @@ export function retiredTypeConfigFilesIn(configDir) {
|
|
|
1680
1716
|
}
|
|
1681
1717
|
return refusals;
|
|
1682
1718
|
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Every `rule-sets/*.toml` whose `[ruleSet] resourceType` is retired (#4959:
|
|
1721
|
+
* `lock`), as `<file>: ruleSet.resourceType = "<type>" — <sentence>`. A file
|
|
1722
|
+
* that does not parse is left to the rule-set validators.
|
|
1723
|
+
*/
|
|
1724
|
+
export function retiredRuleSetFilesIn(configDir) {
|
|
1725
|
+
const dir = join(configDir, "rule-sets");
|
|
1726
|
+
if (!existsSync(dir))
|
|
1727
|
+
return [];
|
|
1728
|
+
const refusals = [];
|
|
1729
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith(".toml")).sort()) {
|
|
1730
|
+
let resourceType;
|
|
1731
|
+
try {
|
|
1732
|
+
resourceType = parseTomlFile(join(dir, file))?.ruleSet?.resourceType;
|
|
1733
|
+
}
|
|
1734
|
+
catch {
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
const retired = retiredRuleSetResourceType(resourceType);
|
|
1738
|
+
if (!retired)
|
|
1739
|
+
continue;
|
|
1740
|
+
refusals.push(`${file}: ruleSet.resourceType = ${JSON.stringify(resourceType)} — ${retired.message}`);
|
|
1741
|
+
}
|
|
1742
|
+
return refusals;
|
|
1743
|
+
}
|
|
1683
1744
|
/**
|
|
1684
1745
|
* `config push`'s preflight — every check the authored files decide on their
|
|
1685
1746
|
* own, run before the FIRST mutating call (#976 fix A, #3320, #3375).
|
|
@@ -1755,6 +1816,13 @@ export async function runConfigPushPreflight(input) {
|
|
|
1755
1816
|
}
|
|
1756
1817
|
if (retiredTypeConfigFiles.length > 0)
|
|
1757
1818
|
abortOnPreflightErrors();
|
|
1819
|
+
// #4959 — a rule set of a retired resource type (`lock`) is refused here,
|
|
1820
|
+
// with the sentence the rule-set routes give, before the rule-set loop could
|
|
1821
|
+
// skip it as unchanged. Deleting the file and pushing with `--prune` removes
|
|
1822
|
+
// the stored row.
|
|
1823
|
+
for (const refusal of retiredRuleSetFilesIn(configDir)) {
|
|
1824
|
+
preflightValidationErrors.push(` ${refusal}`);
|
|
1825
|
+
}
|
|
1758
1826
|
// Validate all database-type TOMLs: the parse itself refuses an invalid
|
|
1759
1827
|
// declared-access manifest. (#3189 — the `[[operations]]` `$params` and
|
|
1760
1828
|
// `[[subscriptions]]` checks retired with the two tables, which the
|
|
@@ -1776,9 +1844,12 @@ export async function runConfigPushPreflight(input) {
|
|
|
1776
1844
|
// refused here, naming both files, before anything is applied.
|
|
1777
1845
|
const preflightDb3TypesDir = join(configDir, "db3-types");
|
|
1778
1846
|
if (existsSync(preflightDb3TypesDir)) {
|
|
1847
|
+
// #5025 — each file as push sends it: with the models it names.
|
|
1848
|
+
const preflightDb3Resolution = resolveDb3TypeModels(preflightDb3TypesDir);
|
|
1779
1849
|
for (const file of selectFiles("db3-type", preflightDb3TypesDir, readdirSync(preflightDb3TypesDir).filter((f) => f.endsWith(".toml")))) {
|
|
1780
1850
|
try {
|
|
1781
|
-
|
|
1851
|
+
const parsed = parseTomlFile(join(preflightDb3TypesDir, file));
|
|
1852
|
+
parseDb3TypeToml(preflightDb3Resolution.documents.get(file) ?? parsed);
|
|
1782
1853
|
}
|
|
1783
1854
|
catch (err) {
|
|
1784
1855
|
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
@@ -1983,7 +2054,10 @@ export async function runConfigPushPreflight(input) {
|
|
|
1983
2054
|
// cannot disagree about which files are pushable.
|
|
1984
2055
|
for (const rejected of collectConfigFileValidationErrors(configDir)) {
|
|
1985
2056
|
for (const message of rejected.messages) {
|
|
1986
|
-
|
|
2057
|
+
// #5025 — a refusal about two db3 type files is collected for each.
|
|
2058
|
+
if (!preflightValidationErrors.includes(` ${message}`)) {
|
|
2059
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2060
|
+
}
|
|
1987
2061
|
}
|
|
1988
2062
|
}
|
|
1989
2063
|
// #4872 — a deprecated key is accepted and never sent; the file is told
|
|
@@ -2901,6 +2975,29 @@ export function collectConfigFileValidationErrors(configDir) {
|
|
|
2901
2975
|
messages,
|
|
2902
2976
|
});
|
|
2903
2977
|
}
|
|
2978
|
+
// #5025 — a db3 type naming a model declared in another file of the
|
|
2979
|
+
// directory: an undeclared or self reference, or two files declaring one
|
|
2980
|
+
// model differently, refuses every file it is about.
|
|
2981
|
+
if (dir === "db3-types") {
|
|
2982
|
+
for (const refusal of resolveDb3TypeModels(typeDir).refusals) {
|
|
2983
|
+
for (const file of refusal.files) {
|
|
2984
|
+
const filePath = join(typeDir, file);
|
|
2985
|
+
let tomlData;
|
|
2986
|
+
try {
|
|
2987
|
+
tomlData = parseTomlFile(filePath);
|
|
2988
|
+
}
|
|
2989
|
+
catch {
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
found.push({
|
|
2993
|
+
type: surface.label,
|
|
2994
|
+
key: localConfigRowKey(surface.label, tomlData, file),
|
|
2995
|
+
filePath,
|
|
2996
|
+
messages: [refusal.message],
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
2904
3001
|
}
|
|
2905
3002
|
return found;
|
|
2906
3003
|
}
|
|
@@ -4134,6 +4231,42 @@ export const DB3_TYPE_DIFF_SPEC = {
|
|
|
4134
4231
|
serialize: (record, maps) => serializeDb3Type(record, maps.ruleSetIdToName),
|
|
4135
4232
|
describe: (entity) => `db3 type "${entity?.databaseType}"`,
|
|
4136
4233
|
};
|
|
4234
|
+
/**
|
|
4235
|
+
* #5025 — what a `config push` conflict on `db3-types/<name>.toml` says when
|
|
4236
|
+
* the server holds a type of that name this tree never pulled: a database
|
|
4237
|
+
* type (`POST databases/types`), or a db3 type pushed from another tree.
|
|
4238
|
+
*/
|
|
4239
|
+
export function db3TypeConflictHint(name) {
|
|
4240
|
+
return (`The server already holds a type named "${name}" (a database type, or a db3 type this ` +
|
|
4241
|
+
`tree never pulled). Run \`primitive config pull\` to adopt it as db3-types/${name}.toml, ` +
|
|
4242
|
+
`or \`primitive config push --force\` to overwrite it with this file.`);
|
|
4243
|
+
}
|
|
4244
|
+
/**
|
|
4245
|
+
* #5025 — the baseline a successful `PUT db3-types/:name` stamps: the semantic
|
|
4246
|
+
* hash of the row its answer carries, read as the live listing spells it. A
|
|
4247
|
+
* file naming shared models reads differently whenever the declaring file
|
|
4248
|
+
* moves, while its own bytes stand still, so the byte hash alone would read
|
|
4249
|
+
* the next such edit as both sides having changed.
|
|
4250
|
+
*/
|
|
4251
|
+
export function db3TypeWriteBaseline(answerType, name, maps) {
|
|
4252
|
+
if (!answerType || typeof answerType !== "object")
|
|
4253
|
+
return undefined;
|
|
4254
|
+
const { name: typeName, revision: _revision, ...row } = answerType;
|
|
4255
|
+
return remoteSemanticBaseline({
|
|
4256
|
+
spec: DB3_TYPE_DIFF_SPEC,
|
|
4257
|
+
maps,
|
|
4258
|
+
returned: { ...row, databaseType: row.databaseType ?? typeName ?? name },
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* #5025 — the hint a declined `db3-types/` push carries: the conflict hint
|
|
4263
|
+
* when the row is live but this tree holds no baseline for it, none otherwise.
|
|
4264
|
+
*/
|
|
4265
|
+
export function db3TypeDeclineHint(name, outcome) {
|
|
4266
|
+
return outcome.action === "conflict" && outcome.direction === "unknown-direction"
|
|
4267
|
+
? db3TypeConflictHint(name)
|
|
4268
|
+
: undefined;
|
|
4269
|
+
}
|
|
4137
4270
|
/**
|
|
4138
4271
|
* #4802 — the names a config tree holds in BOTH `db3-types/` and
|
|
4139
4272
|
* `database-type-configs/`, each one resource. Twins whose files read the same
|
|
@@ -4150,6 +4283,8 @@ export function classifyDb3TypeTwins(configDir, maps = { ruleSetIdToName: new Ma
|
|
|
4150
4283
|
if (!existsSync(db3Dir) || !existsSync(legacyDir))
|
|
4151
4284
|
return { agreeing, refusals, refusedNames };
|
|
4152
4285
|
const legacy = new Map();
|
|
4286
|
+
// #5025 — the db3 side as push sends it, with the models it names.
|
|
4287
|
+
const resolved = resolveDb3TypeModels(db3Dir).documents;
|
|
4153
4288
|
for (const file of readdirSync(legacyDir).filter((f) => f.endsWith(".toml"))) {
|
|
4154
4289
|
try {
|
|
4155
4290
|
const parsed = parseTomlFile(join(legacyDir, file));
|
|
@@ -4167,6 +4302,7 @@ export function classifyDb3TypeTwins(configDir, maps = { ruleSetIdToName: new Ma
|
|
|
4167
4302
|
catch {
|
|
4168
4303
|
continue;
|
|
4169
4304
|
}
|
|
4305
|
+
parsed = resolved.get(file) ?? parsed;
|
|
4170
4306
|
const name = parsed?.type?.name || basename(file, ".toml");
|
|
4171
4307
|
const twin = legacy.get(name);
|
|
4172
4308
|
if (!twin)
|
|
@@ -7794,11 +7930,42 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7794
7930
|
}
|
|
7795
7931
|
// #4802 — db3 types: the same rows, named by `name`.
|
|
7796
7932
|
const db3TypeEntities = {};
|
|
7933
|
+
const db3TypeContents = new Map();
|
|
7934
|
+
for (const typeConfig of db3TypesToWrite) {
|
|
7935
|
+
db3TypeContents.set(`${typeConfig.databaseType}.toml`, serializeDb3Type(typeConfig, ruleSetIdToName, { logger: (msg) => info(` ${msg}`) }));
|
|
7936
|
+
}
|
|
7937
|
+
// #5025 — a local file naming models declared in another file is kept
|
|
7938
|
+
// when, resolved against the tree this pull leaves, it reads as its
|
|
7939
|
+
// server row; decided for every file before any is written. The tree
|
|
7940
|
+
// it leaves has no file the prune below removes, so a declaring type
|
|
7941
|
+
// deleted on the server is planned out here (a file the prune then
|
|
7942
|
+
// keeps for its local edits only costs a rewrite in full).
|
|
7943
|
+
const db3TypeResource = SYNC_RESOURCE_TYPES.find((type) => type.stateSlot === "db3Types");
|
|
7944
|
+
const db3PrunePlan = planPrune({
|
|
7945
|
+
resourceType: db3TypeResource,
|
|
7946
|
+
priorKeys: Object.keys(priorEntities.db3Types ?? {}).filter((key) => selectionIncludesKey(pullSelection, db3TypeResource.label, key)),
|
|
7947
|
+
presence: databaseTypeConfigsOutcome.ok
|
|
7948
|
+
? { ok: true, serverKeys: db3TypesToWrite.map((t) => t.databaseType) }
|
|
7949
|
+
: { ok: false },
|
|
7950
|
+
enabled: options.prune !== false,
|
|
7951
|
+
});
|
|
7952
|
+
const db3PullPlan = planDb3TypePull({
|
|
7953
|
+
local: readDb3TypesDir(join(configDir, "db3-types")),
|
|
7954
|
+
server: db3TypeContents,
|
|
7955
|
+
same: (serverContent, document) => sameDb3TypeAs(serverContent, pullDiffMaps)(stringifyConfigToml(document)),
|
|
7956
|
+
removed: db3PrunePlan.keys.map((key) => db3TypeResource.fileFromKey(key)),
|
|
7957
|
+
});
|
|
7797
7958
|
for (const typeConfig of db3TypesToWrite) {
|
|
7798
7959
|
const filename = `${typeConfig.databaseType}.toml`;
|
|
7799
7960
|
const filePath = join(configDir, "db3-types", filename);
|
|
7800
|
-
const content =
|
|
7801
|
-
const
|
|
7961
|
+
const content = db3TypeContents.get(filename);
|
|
7962
|
+
const kept = db3PullPlan.keep.has(filename);
|
|
7963
|
+
if (kept)
|
|
7964
|
+
pullWrites.unchanged += 1;
|
|
7965
|
+
// A reference-form file the plan rewrites is compared by bytes only:
|
|
7966
|
+
// unresolved, it can read as the server row while what it pushes does not.
|
|
7967
|
+
const matches = db3PullPlan.rewrite.has(filename) ? undefined : sameDb3TypeAs(content, pullDiffMaps);
|
|
7968
|
+
const written = !kept && writePulledFile(filePath, content, pullWrites, matches);
|
|
7802
7969
|
let semanticHash;
|
|
7803
7970
|
try {
|
|
7804
7971
|
semanticHash = hashLocalConfigForDiff(DB3_TYPE_DIFF_SPEC, parseConfigToml(content), pullDiffMaps);
|
|
@@ -7816,6 +7983,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7816
7983
|
if (written)
|
|
7817
7984
|
info(` Wrote db3-types/${filename}`);
|
|
7818
7985
|
}
|
|
7986
|
+
for (const note of db3PullPlan.notes)
|
|
7987
|
+
warn(` Note: ${note}`);
|
|
7819
7988
|
// #3182 — the typed-handle artifacts are REGENERATED, never pulled:
|
|
7820
7989
|
// they are not in the envelope, and a fresh clone must end up with the
|
|
7821
7990
|
// same three files a push would have written. Written here, after the
|
|
@@ -8399,6 +8568,8 @@ What a push guarantees (two stages):
|
|
|
8399
8568
|
else if (conflict.direction === "late-409") {
|
|
8400
8569
|
console.log(` ${chalk.dim("the server changed between this push's read and its write")}`);
|
|
8401
8570
|
}
|
|
8571
|
+
if (conflict.hint)
|
|
8572
|
+
console.log(` ${conflict.hint}`);
|
|
8402
8573
|
for (const field of conflict.fields || []) {
|
|
8403
8574
|
console.log(` ${field.field}: local ${summarizeFieldValue(field.local)} | server ${summarizeFieldValue(field.server)}`);
|
|
8404
8575
|
}
|
|
@@ -8546,7 +8717,7 @@ What a push guarantees (two stages):
|
|
|
8546
8717
|
warn(` ${label}: live state unavailable (${message}) — falling back to last-sync content hashes`);
|
|
8547
8718
|
};
|
|
8548
8719
|
/** Record a decision push refused to apply, with its per-field diff. */
|
|
8549
|
-
const recordDeclined = (type, key, outcome, storedModifiedAt) => {
|
|
8720
|
+
const recordDeclined = (type, key, outcome, storedModifiedAt, hint) => {
|
|
8550
8721
|
if (outcome.action === "drift") {
|
|
8551
8722
|
drifted.push({ type, key });
|
|
8552
8723
|
return;
|
|
@@ -8595,6 +8766,7 @@ What a push guarantees (two stages):
|
|
|
8595
8766
|
direction: outcome.direction,
|
|
8596
8767
|
fields: outcome.fields,
|
|
8597
8768
|
planned: false,
|
|
8769
|
+
...(hint ? { hint } : {}),
|
|
8598
8770
|
});
|
|
8599
8771
|
};
|
|
8600
8772
|
/**
|
|
@@ -8747,6 +8919,39 @@ What a push guarantees (two stages):
|
|
|
8747
8919
|
if (varsOverrideKeys && varsOverrideFile) {
|
|
8748
8920
|
info(varsOverrideSummaryLine(varsOverrideFile, varsOverrideKeys));
|
|
8749
8921
|
}
|
|
8922
|
+
// #4989 — a prune whose plan hard-deletes needs `admin`. A session
|
|
8923
|
+
// without it is refused here, before anything below mutates, rather
|
|
8924
|
+
// than having each delete refused after the rest of the push landed.
|
|
8925
|
+
// A session whose scope cannot be read is refused too: assuming
|
|
8926
|
+
// `admin` would let the push land halfway all the same.
|
|
8927
|
+
if (options.prune) {
|
|
8928
|
+
const refusal = pruneAdminScopeRefusal(await planPushPrune({
|
|
8929
|
+
specs: PUSH_PRUNE_SPECS.filter((spec) => spec.deleteNeedsAdmin),
|
|
8930
|
+
configDir,
|
|
8931
|
+
entities: syncState?.entities,
|
|
8932
|
+
includes: (label, key) => selectionIncludesKey(pushSelection, label, key),
|
|
8933
|
+
client,
|
|
8934
|
+
appId: resolvedAppId,
|
|
8935
|
+
force: !!options.force,
|
|
8936
|
+
}));
|
|
8937
|
+
let stop = null;
|
|
8938
|
+
if (refusal) {
|
|
8939
|
+
try {
|
|
8940
|
+
if (!(await sessionHoldsAdminFor(client, resolvedAppId)))
|
|
8941
|
+
stop = refusal;
|
|
8942
|
+
}
|
|
8943
|
+
catch (err) {
|
|
8944
|
+
stop = pruneScopeUnknownRefusal(err?.message ?? String(err));
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
if (stop && options.dryRun) {
|
|
8948
|
+
warn(stop);
|
|
8949
|
+
}
|
|
8950
|
+
else if (stop) {
|
|
8951
|
+
error(stop);
|
|
8952
|
+
process.exit(1);
|
|
8953
|
+
}
|
|
8954
|
+
}
|
|
8750
8955
|
// Re-derived, not returned: both are one-line functions of inputs the
|
|
8751
8956
|
// entry point already took, and widening its return type to carry them
|
|
8752
8957
|
// would make the vars apply leg depend on the preflight's internals.
|
|
@@ -10731,9 +10936,11 @@ What a push guarantees (two stages):
|
|
|
10731
10936
|
else if (files.length > 0) {
|
|
10732
10937
|
warnLiveUnavailable("db3-types", "rule sets unavailable");
|
|
10733
10938
|
}
|
|
10939
|
+
// #5025 — each file with the models it names in another file.
|
|
10940
|
+
const db3Resolution = resolveDb3TypeModels(db3TypesDir);
|
|
10734
10941
|
for (const file of files) {
|
|
10735
10942
|
const filePath = join(db3TypesDir, file);
|
|
10736
|
-
const tomlData = parseTomlFile(filePath);
|
|
10943
|
+
const tomlData = db3Resolution.documents.get(file) ?? parseTomlFile(filePath);
|
|
10737
10944
|
const { typeConfig } = parseDb3TypeToml(tomlData);
|
|
10738
10945
|
const name = typeConfig.databaseType || basename(file, ".toml");
|
|
10739
10946
|
if (agreeingTwins.has(name))
|
|
@@ -10780,7 +10987,7 @@ What a push guarantees (two stages):
|
|
|
10780
10987
|
continue;
|
|
10781
10988
|
}
|
|
10782
10989
|
if (gate.action === "drift" || gate.action === "conflict" || gate.action === "immutable") {
|
|
10783
|
-
recordDeclined("db3-type", name, gate, entry?.modifiedAt);
|
|
10990
|
+
recordDeclined("db3-type", name, gate, entry?.modifiedAt, db3TypeDeclineHint(name, gate));
|
|
10784
10991
|
continue;
|
|
10785
10992
|
}
|
|
10786
10993
|
}
|
|
@@ -10828,11 +11035,18 @@ What a push guarantees (two stages):
|
|
|
10828
11035
|
else if (answer.push?.status === "not_started") {
|
|
10829
11036
|
warn(` The push to the db3s of ${name} did not start: ${answer.push.error}`);
|
|
10830
11037
|
}
|
|
10831
|
-
record(answer.type?.modifiedAt || new Date().toISOString(),
|
|
11038
|
+
record(answer.type?.modifiedAt || new Date().toISOString(), db3TypeWriteBaseline(answer.type, name, pushDiffMaps), typeof answer.type?.schema === "string" && answer.type.schema.length > 0);
|
|
10832
11039
|
}
|
|
10833
11040
|
catch (err) {
|
|
10834
11041
|
if (err instanceof ConflictError) {
|
|
10835
|
-
conflicts.push({
|
|
11042
|
+
conflicts.push({
|
|
11043
|
+
type: "db3-type",
|
|
11044
|
+
key: name,
|
|
11045
|
+
serverModifiedAt: err.serverModifiedAt,
|
|
11046
|
+
localModifiedAt: expectedModifiedAt || "unknown",
|
|
11047
|
+
// #5025 — a create the server refused: it holds the name.
|
|
11048
|
+
...(exists ? {} : { hint: db3TypeConflictHint(name) }),
|
|
11049
|
+
});
|
|
10836
11050
|
continue;
|
|
10837
11051
|
}
|
|
10838
11052
|
throw wrapEntityError(err, exists ? "update" : "create", "db3 type", name);
|
|
@@ -12426,65 +12640,25 @@ What a push guarantees (two stages):
|
|
|
12426
12640
|
const toDelete = [];
|
|
12427
12641
|
const alreadyAbsent = [];
|
|
12428
12642
|
const skipped = [];
|
|
12429
|
-
for (const
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
configDir,
|
|
12434
|
-
priorEntities,
|
|
12435
|
-
}).filter(({ key }) =>
|
|
12643
|
+
for (const item of await planPushPrune({
|
|
12644
|
+
specs: PUSH_PRUNE_SPECS,
|
|
12645
|
+
configDir,
|
|
12646
|
+
entities: syncState?.entities,
|
|
12436
12647
|
// `--only` bounds prune to the selection. Without this a
|
|
12437
12648
|
// single-object push with `--prune` would delete every managed
|
|
12438
12649
|
// entity whose file happens to be missing — objects the operator
|
|
12439
12650
|
// did not name and is not looking at.
|
|
12440
|
-
selectionIncludesKey(pushSelection,
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
priorModifiedAt: entry?.modifiedAt,
|
|
12452
|
-
fetch: { ok: false },
|
|
12453
|
-
force: !!options.force,
|
|
12454
|
-
});
|
|
12455
|
-
skipped.push({ spec, key, entry, address, decision });
|
|
12456
|
-
continue;
|
|
12457
|
-
}
|
|
12458
|
-
// Point read: confirm live presence + drift. Fail closed — any
|
|
12459
|
-
// non-404 error leaves the entity and its state slot untouched.
|
|
12460
|
-
let fetchOutcome;
|
|
12461
|
-
try {
|
|
12462
|
-
const detail = await spec.fetchDetail(client, resolvedAppId, address);
|
|
12463
|
-
fetchOutcome = { ok: true, detail };
|
|
12464
|
-
}
|
|
12465
|
-
catch (err) {
|
|
12466
|
-
if (err instanceof ApiError && err.statusCode === 404) {
|
|
12467
|
-
fetchOutcome = { ok: false, status: 404 };
|
|
12468
|
-
}
|
|
12469
|
-
else {
|
|
12470
|
-
fetchOutcome = { ok: false };
|
|
12471
|
-
}
|
|
12472
|
-
}
|
|
12473
|
-
const decision = decidePushPruneCandidate({
|
|
12474
|
-
spec,
|
|
12475
|
-
address,
|
|
12476
|
-
priorModifiedAt: entry?.modifiedAt,
|
|
12477
|
-
fetch: fetchOutcome,
|
|
12478
|
-
force: !!options.force,
|
|
12479
|
-
});
|
|
12480
|
-
const item = { spec, key, entry, address, decision };
|
|
12481
|
-
if (decision.action === "delete")
|
|
12482
|
-
toDelete.push(item);
|
|
12483
|
-
else if (decision.action === "drop-state")
|
|
12484
|
-
alreadyAbsent.push(item);
|
|
12485
|
-
else
|
|
12486
|
-
skipped.push(item);
|
|
12487
|
-
}
|
|
12651
|
+
includes: (label, key) => selectionIncludesKey(pushSelection, label, key),
|
|
12652
|
+
client,
|
|
12653
|
+
appId: resolvedAppId,
|
|
12654
|
+
force: !!options.force,
|
|
12655
|
+
})) {
|
|
12656
|
+
if (item.decision.action === "delete")
|
|
12657
|
+
toDelete.push(item);
|
|
12658
|
+
else if (item.decision.action === "drop-state")
|
|
12659
|
+
alreadyAbsent.push(item);
|
|
12660
|
+
else
|
|
12661
|
+
skipped.push(item);
|
|
12488
12662
|
}
|
|
12489
12663
|
const totalPlanned = toDelete.length +
|
|
12490
12664
|
alreadyAbsent.length +
|
|
@@ -12570,6 +12744,7 @@ What a push guarantees (two stages):
|
|
|
12570
12744
|
else {
|
|
12571
12745
|
let prunedCount = 0;
|
|
12572
12746
|
let blockedCount = 0;
|
|
12747
|
+
let failedCount = 0;
|
|
12573
12748
|
// Entities already gone server-side: drop their stale state (2).
|
|
12574
12749
|
for (const item of alreadyAbsent) {
|
|
12575
12750
|
dropPruneState(item.spec, item.key);
|
|
@@ -12603,8 +12778,16 @@ What a push guarantees (two stages):
|
|
|
12603
12778
|
warn(` Blocked — ${item.spec.resourceType.label} ${item.key} is still referenced: ${message}`);
|
|
12604
12779
|
break;
|
|
12605
12780
|
default:
|
|
12606
|
-
|
|
12607
|
-
|
|
12781
|
+
// #4989 — any other refusal fails the push with the
|
|
12782
|
+
// server's error, as every other apply failure does
|
|
12783
|
+
// (#2731 B2), and keeps the state so a re-run retries.
|
|
12784
|
+
failedCount++;
|
|
12785
|
+
applyFailures.push({
|
|
12786
|
+
type: item.spec.resourceType.label,
|
|
12787
|
+
key: item.key,
|
|
12788
|
+
message,
|
|
12789
|
+
});
|
|
12790
|
+
error(` Failed — could not delete ${item.spec.resourceType.label} ${item.key}: ${message}`);
|
|
12608
12791
|
}
|
|
12609
12792
|
}
|
|
12610
12793
|
}
|
|
@@ -12621,7 +12804,7 @@ What a push guarantees (two stages):
|
|
|
12621
12804
|
});
|
|
12622
12805
|
prunedCount += testCasePrune.deleted + testCasePrune.deletedAttachments;
|
|
12623
12806
|
if (totalPlanned > 0) {
|
|
12624
|
-
info(`Prune: ${prunedCount} deleted, ${alreadyAbsent.length} already absent, ${skipped.length + blockedCount} skipped.`);
|
|
12807
|
+
info(`Prune: ${prunedCount} deleted, ${alreadyAbsent.length} already absent, ${skipped.length + blockedCount} skipped${failedCount > 0 ? `, ${failedCount} failed` : ""}.`);
|
|
12625
12808
|
}
|
|
12626
12809
|
}
|
|
12627
12810
|
}
|
|
@@ -12969,8 +13152,10 @@ What a push guarantees (two stages):
|
|
|
12969
13152
|
}
|
|
12970
13153
|
const db3TypesDiffDir = join(configDir, "db3-types");
|
|
12971
13154
|
if (databaseTypeConfigsOutcomeDiff.ok && existsSync(db3TypesDiffDir)) {
|
|
13155
|
+
// #5025 — compared as push sends it, with the models it names.
|
|
13156
|
+
const db3DiffResolution = resolveDb3TypeModels(db3TypesDiffDir);
|
|
12972
13157
|
for (const file of readdirSync(db3TypesDiffDir).filter((f) => f.endsWith(".toml"))) {
|
|
12973
|
-
const tomlData = parseTomlFile(join(db3TypesDiffDir, file));
|
|
13158
|
+
const tomlData = cloneTomlValue(db3DiffResolution.documents.get(file) ?? parseTomlFile(join(db3TypesDiffDir, file)));
|
|
12974
13159
|
const key = tomlData.type?.name || basename(file, ".toml");
|
|
12975
13160
|
if (!tomlData.type)
|
|
12976
13161
|
tomlData.type = {};
|