primitive-admin 1.2.0-alpha.4 → 1.2.0-alpha.5
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/assets/skill/skills/primitive-platform/SKILL.md +17 -0
- package/dist/src/commands/auth.js +7 -4
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/connections.js +11 -12
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +2 -76
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/database-types.js +313 -13
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +9 -0
- package/dist/src/commands/databases.js +67 -367
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +32 -2
- package/dist/src/commands/documents.js +281 -685
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +2 -2
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/llm.js +6 -9
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/settings.d.ts +9 -12
- package/dist/src/commands/settings.js +79 -31
- package/dist/src/commands/settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +31 -81
- package/dist/src/commands/sync.js +228 -1113
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +30 -199
- package/dist/src/lib/api-client.js +31 -297
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -7
- package/dist/src/lib/codegen-shared/generatedFiles.js +2 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +1 -1
- package/dist/src/lib/codegen-shared/prettierStable.js +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +2 -2
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +2 -2
- package/dist/src/lib/config-object-descriptor.js +1 -5
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-surface.d.ts +2 -1
- package/dist/src/lib/config-surface.js +4 -1
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/connections-list.d.ts +10 -0
- package/dist/src/lib/connections-list.js +18 -0
- package/dist/src/lib/connections-list.js.map +1 -0
- package/dist/src/lib/csv.d.ts +8 -9
- package/dist/src/lib/csv.js +3 -4
- package/dist/src/lib/csv.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +2 -1
- package/dist/src/lib/db-codegen/dbNaming.js +2 -1
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +2 -2
- package/dist/src/lib/db-codegen/dbTsTypes.js +2 -2
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +59 -1
- package/dist/src/lib/db-codegen/dbTypeIR.js +12 -1
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
- package/dist/src/lib/document-export-record-log.d.ts +57 -0
- package/dist/src/lib/document-export-record-log.js +215 -0
- package/dist/src/lib/document-export-record-log.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +13 -2
- package/dist/src/lib/document-ingest-artifact.js +7 -3
- package/dist/src/lib/document-ingest-artifact.js.map +1 -1
- package/dist/src/lib/document-ingest-rows.d.ts +16 -0
- package/dist/src/lib/document-ingest-rows.js +17 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -1
- package/dist/src/lib/document-ingest.d.ts +22 -0
- package/dist/src/lib/document-ingest.js +33 -0
- package/dist/src/lib/document-ingest.js.map +1 -1
- package/dist/src/lib/function-triggers.d.ts +3 -15
- package/dist/src/lib/function-triggers.js +10 -72
- package/dist/src/lib/function-triggers.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +0 -20
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +40 -2
- package/dist/src/lib/generated-config-surfaces.js +160 -94
- 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/login-scope-lines.d.ts +15 -0
- package/dist/src/lib/login-scope-lines.js +22 -0
- package/dist/src/lib/login-scope-lines.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +0 -5
- package/dist/src/lib/paginate.js +0 -5
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/refresh-admin-credentials.d.ts +7 -0
- package/dist/src/lib/refresh-admin-credentials.js +16 -4
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
- package/dist/src/lib/root-import-decision.d.ts +15 -14
- package/dist/src/lib/root-import-decision.js +44 -51
- package/dist/src/lib/root-import-decision.js.map +1 -1
- package/dist/src/lib/snapshot-build-rows.d.ts +21 -0
- package/dist/src/lib/snapshot-build-rows.js +22 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -1
- package/dist/src/lib/snapshot-build.js +8 -3
- package/dist/src/lib/snapshot-build.js.map +1 -1
- package/dist/src/lib/swift-codegen/banners.d.ts +34 -7
- package/dist/src/lib/swift-codegen/banners.js +37 -7
- package/dist/src/lib/swift-codegen/banners.js.map +1 -1
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +3 -3
- package/dist/src/lib/swift-codegen/functionGenerator.js +22 -17
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.js +27 -27
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/toml-database-config.d.ts +14 -75
- package/dist/src/lib/toml-database-config.js +14 -305
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.js +15 -1
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/package.json +3 -5
- package/dist/src/lib/generated-workflow-model-fields.d.ts +0 -20
- package/dist/src/lib/generated-workflow-model-fields.js +0 -53
- package/dist/src/lib/generated-workflow-model-fields.js.map +0 -1
- package/dist/src/lib/workflow-field-descriptor.d.ts +0 -91
- package/dist/src/lib/workflow-field-descriptor.js +0 -153
- package/dist/src/lib/workflow-field-descriptor.js.map +0 -1
|
@@ -2,15 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlink
|
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
4
|
import { lookup as mimeLookup } from "mime-types";
|
|
5
|
-
import { ApiClient, ApiError, ConflictError,
|
|
5
|
+
import { ApiClient, ApiError, ConflictError, TomlParseError, } from "../lib/api-client.js";
|
|
6
6
|
import { retryOnStoragePending } from "../lib/storage-pending-retry.js";
|
|
7
|
-
import { buildDatabaseTypeTomlData
|
|
7
|
+
import { buildDatabaseTypeTomlData } from "../lib/toml-database-config.js";
|
|
8
8
|
import { canEmitNative } from "../lib/toml-native-form.js";
|
|
9
9
|
// #1446's `type: "json"` emission rule, shared with `serializeFunction` since
|
|
10
10
|
// #3317 — see `config-json-field.ts`.
|
|
11
11
|
import { serializeJsonField } from "../lib/config-json-field.js";
|
|
12
12
|
import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMetadataCategoryConfig, parseDeclaredAccessManifestToml, validateDeclaredAccessManifest, serializeDeclaredAccessManifest, } from "../lib/toml-metadata-config.js";
|
|
13
|
-
import { validateOperations, formatIssue, } from "../lib/toml-params-validator.js";
|
|
14
13
|
import { isPlatformOwnedWorkflow, isPlatformOwnedWorkflowKey, } from "../lib/platform-owned.js";
|
|
15
14
|
import { getCurrentAppId, getServerUrl } from "../lib/config.js";
|
|
16
15
|
import { requireCurrentEnvironment } from "../lib/env-resolver.js";
|
|
@@ -35,7 +34,7 @@ import { buildWorkflowPayloadFromToml, parseWorkflowExpressionsToml, } from "../
|
|
|
35
34
|
// payload builder so the diff normalizer and the wire cannot disagree.
|
|
36
35
|
import { WORKFLOW_MODEL_DEFAULTS } from "../lib/workflow-defaults.js";
|
|
37
36
|
// #2644 — the one definition per configuration object, vendored from the server.
|
|
38
|
-
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredTomlKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
37
|
+
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredTomlKey, retiredDocumentKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
39
38
|
import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
|
|
40
39
|
import { CONFIG_SURFACES, isEmptyForEmit, tomlGroupsOf, } from "../lib/generated-config-surfaces.js";
|
|
41
40
|
// #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
|
|
@@ -1915,33 +1914,33 @@ export function serializePrompt(prompt, logger) {
|
|
|
1915
1914
|
return stringifyConfigToml(data);
|
|
1916
1915
|
}
|
|
1917
1916
|
/**
|
|
1918
|
-
* The `config push` warning for a
|
|
1917
|
+
* The `config push` warning for a caller workflow pushed with no `accessRule`
|
|
1918
|
+
* (#2652).
|
|
1919
1919
|
*
|
|
1920
1920
|
* Returns `null` when a rule is declared — and for a `runAs = "system"`
|
|
1921
1921
|
* workflow, which rejects any rule and cannot be started by a member at all.
|
|
1922
|
-
* Otherwise it names the
|
|
1923
|
-
* because after the #2652 flip a ruleless
|
|
1924
|
-
*
|
|
1922
|
+
* Otherwise it names the workflow, the consequence, and the one-line restore,
|
|
1923
|
+
* because after the #2652 flip a ruleless caller workflow denies every
|
|
1924
|
+
* non-admin caller from the deploy onward.
|
|
1925
1925
|
*
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
1931
|
-
*
|
|
1926
|
+
* A prompt gets no warning (#4368): a server function runs it on its
|
|
1927
|
+
* `prompt:<key>` grant, which never consults the rule, so a ruleless prompt is
|
|
1928
|
+
* the ordinary case, not a lockout.
|
|
1929
|
+
*
|
|
1930
|
+
* The workflow pushes `accessRule` as value-or-`null` since #2743's
|
|
1931
|
+
* always-send conversion, so an omitted key really does leave it ruleless on
|
|
1932
|
+
* the server. The wording says so explicitly, because it used to say the
|
|
1933
|
+
* opposite (the update path preserved a stored rule) and an operator who
|
|
1934
|
+
* learned that caveat needs to see it retracted.
|
|
1932
1935
|
*/
|
|
1933
1936
|
export function missingAccessRuleWarning(opts) {
|
|
1934
1937
|
if (typeof opts.accessRule === "string" && opts.accessRule.trim() !== "") {
|
|
1935
1938
|
return null;
|
|
1936
1939
|
}
|
|
1937
|
-
if (opts.
|
|
1940
|
+
if (opts.runAs === "system")
|
|
1938
1941
|
return null;
|
|
1939
1942
|
const restore = `Add accessRule = "true" to allow any app member, or a CEL rule ` +
|
|
1940
1943
|
`naming who may call it.`;
|
|
1941
|
-
if (opts.kind === "prompt") {
|
|
1942
|
-
return (`prompt '${opts.key}' has no accessRule — all non-admin executions are ` +
|
|
1943
|
-
`denied. ${restore}`);
|
|
1944
|
-
}
|
|
1945
1944
|
return (`workflow '${opts.key}' declares no accessRule — pushing this file clears ` +
|
|
1946
1945
|
`any rule stored server-side, and a workflow with no stored rule denies ` +
|
|
1947
1946
|
`all non-admin starts. ${restore}`);
|
|
@@ -2328,7 +2327,16 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2328
2327
|
*/
|
|
2329
2328
|
export function configDocumentShapeErrors(filePath, tomlData, surface) {
|
|
2330
2329
|
const errors = [];
|
|
2331
|
-
const
|
|
2330
|
+
const found = findUnknownDocumentKeys(tomlData, surface);
|
|
2331
|
+
// #3189 — a table this file type retired on purpose gets its own sentence,
|
|
2332
|
+
// stated once beside the definitions, not the "check the spelling, or
|
|
2333
|
+
// upgrade the CLI" hint, which is backwards for it.
|
|
2334
|
+
for (const key of found) {
|
|
2335
|
+
const retired = retiredDocumentKey(surface.label, key);
|
|
2336
|
+
if (retired)
|
|
2337
|
+
errors.push(`${filePath}: [${key}] — ${retired.toml}`);
|
|
2338
|
+
}
|
|
2339
|
+
const unknown = found.filter((key) => !retiredDocumentKey(surface.label, key));
|
|
2332
2340
|
if (unknown.length > 0) {
|
|
2333
2341
|
errors.push(unrecognizedPushTablesMessage(filePath, unknown));
|
|
2334
2342
|
}
|
|
@@ -2563,56 +2571,19 @@ export async function runConfigPushPreflight(input) {
|
|
|
2563
2571
|
}
|
|
2564
2572
|
}
|
|
2565
2573
|
}
|
|
2566
|
-
// Validate all database-type TOMLs
|
|
2567
|
-
// access
|
|
2574
|
+
// Validate all database-type TOMLs: the parse itself refuses an invalid
|
|
2575
|
+
// declared-access manifest. (#3189 — the `[[operations]]` `$params` and
|
|
2576
|
+
// `[[subscriptions]]` checks retired with the two tables, which the
|
|
2577
|
+
// document-shape preflight now refuses by name.)
|
|
2568
2578
|
const preflightDbTypesDir = join(configDir, "database-type-configs");
|
|
2569
2579
|
if (existsSync(preflightDbTypesDir)) {
|
|
2570
2580
|
for (const file of selectFiles("database-type-config", preflightDbTypesDir, readdirSync(preflightDbTypesDir).filter((f) => f.endsWith(".toml")))) {
|
|
2571
2581
|
const filePath = join(preflightDbTypesDir, file);
|
|
2572
|
-
let rawToml;
|
|
2573
|
-
let tomlData;
|
|
2574
|
-
try {
|
|
2575
|
-
rawToml = readFileSync(filePath, "utf-8");
|
|
2576
|
-
tomlData = parseTomlFile(filePath);
|
|
2577
|
-
}
|
|
2578
|
-
catch (err) {
|
|
2579
|
-
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2580
|
-
continue;
|
|
2581
|
-
}
|
|
2582
|
-
let operations = [];
|
|
2583
2582
|
try {
|
|
2584
|
-
|
|
2583
|
+
parseDatabaseTypeToml(parseTomlFile(filePath));
|
|
2585
2584
|
}
|
|
2586
2585
|
catch (err) {
|
|
2587
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
2588
|
-
preflightValidationErrors.push(` ${file}: ${err.message}`);
|
|
2589
|
-
continue;
|
|
2590
|
-
}
|
|
2591
2586
|
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2592
|
-
continue;
|
|
2593
|
-
}
|
|
2594
|
-
const validation = validateOperations({
|
|
2595
|
-
filePath,
|
|
2596
|
-
rawToml,
|
|
2597
|
-
operations,
|
|
2598
|
-
});
|
|
2599
|
-
for (const e of validation.errors) {
|
|
2600
|
-
preflightValidationErrors.push(` ${formatIssue(e)}`);
|
|
2601
|
-
}
|
|
2602
|
-
// Issue #1422: non-fatal deprecation notice. A `celContextAccess`
|
|
2603
|
-
// / `metadataAccess` gate on a database type config is a single CEL
|
|
2604
|
-
// expression that gates BOTH read and update of the database CEL
|
|
2605
|
-
// context — granting read implicitly grants update. Steer operators
|
|
2606
|
-
// to resource metadata categories (separate readRule/writeRule).
|
|
2607
|
-
// This is advisory only: the push continues and the runtime gate is
|
|
2608
|
-
// unchanged. It fires only when the deprecated key is actually
|
|
2609
|
-
// present, so metadata-category-only configs never trip it.
|
|
2610
|
-
const typeSection = tomlData.type || {};
|
|
2611
|
-
if (typeSection.celContextAccess || typeSection.metadataAccess) {
|
|
2612
|
-
const usedKey = typeSection.celContextAccess
|
|
2613
|
-
? "celContextAccess"
|
|
2614
|
-
: "metadataAccess";
|
|
2615
|
-
warn(` ${file}: \`${usedKey}\` is deprecated — it gates database CEL context READ and WRITE with one expression (read implies update). Prefer a resource metadata category with separate readRule/writeRule (issue #1420). Sync will continue; the runtime gate is unchanged.`);
|
|
2616
2587
|
}
|
|
2617
2588
|
}
|
|
2618
2589
|
}
|
|
@@ -3177,6 +3148,41 @@ export function partitionPromptFilesForPush(promptsDir, files) {
|
|
|
3177
3148
|
}
|
|
3178
3149
|
return { beforeFunctions, afterFunctions };
|
|
3179
3150
|
}
|
|
3151
|
+
/**
|
|
3152
|
+
* The deferred agent files `config push` can still apply — #4437. An agent
|
|
3153
|
+
* that names a function this push failed to apply, and that the server does
|
|
3154
|
+
* not already hold, would only be refused as naming a function the app does
|
|
3155
|
+
* not have. It is held back instead, with a failure that names the function as
|
|
3156
|
+
* the cause; the function's own failure row says why. A file that does not
|
|
3157
|
+
* parse is left for the prompt leg to report.
|
|
3158
|
+
*/
|
|
3159
|
+
export function holdAgentPromptFilesForUnappliedFunctions(promptsDir, files, unappliedFunctionKeys) {
|
|
3160
|
+
const apply = [];
|
|
3161
|
+
const held = [];
|
|
3162
|
+
for (const file of files) {
|
|
3163
|
+
let prompt;
|
|
3164
|
+
try {
|
|
3165
|
+
prompt = parseTomlFile(join(promptsDir, file))?.prompt;
|
|
3166
|
+
}
|
|
3167
|
+
catch {
|
|
3168
|
+
apply.push(file);
|
|
3169
|
+
continue;
|
|
3170
|
+
}
|
|
3171
|
+
const failed = agentDeclarationFunctionKeys(prompt?.agent).filter((key) => unappliedFunctionKeys.has(key));
|
|
3172
|
+
if (failed.length === 0) {
|
|
3173
|
+
apply.push(file);
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
const names = failed.map((key) => `"${key}"`).join(", ");
|
|
3177
|
+
held.push({
|
|
3178
|
+
key: prompt?.key || basename(file, ".toml"),
|
|
3179
|
+
message: `[prompt.agent] names function${failed.length > 1 ? "s" : ""} ${names}, ` +
|
|
3180
|
+
"which this push could not apply (see the function's failure), so the " +
|
|
3181
|
+
"agent was not pushed. Fix the function and push again.",
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
return { apply, held };
|
|
3185
|
+
}
|
|
3180
3186
|
/**
|
|
3181
3187
|
* The kind of the prompt a `prompts/<key>.tests/` sidecar belongs to, read
|
|
3182
3188
|
* from the prompt's own file — `undefined` for another block type, or when the
|
|
@@ -3663,26 +3669,15 @@ export function serializeWorkflow(workflow, draft, configs, logger) {
|
|
|
3663
3669
|
};
|
|
3664
3670
|
return stringifyConfigToml(data);
|
|
3665
3671
|
}
|
|
3666
|
-
//
|
|
3667
|
-
//
|
|
3668
|
-
//
|
|
3669
|
-
//
|
|
3670
|
-
|
|
3671
|
-
// per file — if the existing file uses the legacy JSON-string form, we
|
|
3672
|
-
// preserve it so we don't generate surprise diffs. New files default to
|
|
3673
|
-
// native. The round-trip serializes with `smol-toml` (TOML 1.0), so
|
|
3674
|
-
// heterogeneous arrays emit natively (issue #1450); only shapes that TOML
|
|
3675
|
-
// genuinely can't represent without data loss (null / undefined / an array
|
|
3676
|
-
// containing null) fall back to a JSON string per field with a log message.
|
|
3677
|
-
//
|
|
3678
|
-
// Issue #666: when the type has a stored `schema` (raw TOML of `[models.*]`
|
|
3679
|
-
// blocks), `buildDatabaseTypeTomlData` parses + merges it into the same TOML
|
|
3680
|
-
// object so the result round-trips as `[models.<Name>.fields.<field>]`.
|
|
3681
|
-
export function serializeDatabaseType(typeConfig, operations, ruleSetIdToName, options = {}) {
|
|
3672
|
+
// Serialize a database type's config file: the `[type]` table, the `[models.*]`
|
|
3673
|
+
// blocks re-parsed from the stored `schema` (issue #666), `[triggers]` and the
|
|
3674
|
+
// declared-access manifest. #3189 — no `[[operations]]` or `[[subscriptions]]`:
|
|
3675
|
+
// both retired.
|
|
3676
|
+
export function serializeDatabaseType(typeConfig, ruleSetIdToName, options = {}) {
|
|
3682
3677
|
// #2644 — name any server key this CLI version does not know, rather than
|
|
3683
3678
|
// dropping it silently on the way into the file.
|
|
3684
3679
|
warnUnrecognizedServerKeys(typeConfig, DATABASE_TYPE_TABLE, `database-type-configs/${typeConfig?.databaseType}.toml`, options.logger);
|
|
3685
|
-
const data = buildDatabaseTypeTomlData(typeConfig,
|
|
3680
|
+
const data = buildDatabaseTypeTomlData(typeConfig, ruleSetIdToName);
|
|
3686
3681
|
return stringifyConfigToml(data);
|
|
3687
3682
|
}
|
|
3688
3683
|
/**
|
|
@@ -3902,10 +3897,9 @@ export function dbTypeOwnedScalars(table = DATABASE_TYPE_TABLE) {
|
|
|
3902
3897
|
];
|
|
3903
3898
|
}
|
|
3904
3899
|
export const DB_TYPE_OWNED_SCALARS = dbTypeOwnedScalars();
|
|
3905
|
-
// User-facing TOML key names for the "Cleared <field>" push output
|
|
3906
|
-
// field differs from the key the user writes
|
|
3900
|
+
// User-facing TOML key names for the "Cleared <field>" push output, for the
|
|
3901
|
+
// field whose wire name differs from the key the user writes.
|
|
3907
3902
|
const DB_TYPE_FIELD_LABELS = {
|
|
3908
|
-
metadataAccess: "celContextAccess",
|
|
3909
3903
|
ruleSetId: "ruleSetName",
|
|
3910
3904
|
};
|
|
3911
3905
|
/** The user-facing TOML key for a wire field (for push output). */
|
|
@@ -3978,13 +3972,6 @@ export function ownedScalarsBeingCleared(updateData, serverConfig) {
|
|
|
3978
3972
|
* still comes from the handler that knows what is wrong with it.
|
|
3979
3973
|
*/
|
|
3980
3974
|
function canonicalizeDatabaseTypeConfig(typeConfig) {
|
|
3981
|
-
// CEL fields: trimmed by the handler, and a whitespace-only expression is
|
|
3982
|
-
// stored as none at all — which the projection reads as unset either way.
|
|
3983
|
-
for (const key of ["defaultAccess", "metadataAccess"]) {
|
|
3984
|
-
if (typeof typeConfig[key] === "string") {
|
|
3985
|
-
typeConfig[key] = typeConfig[key].trim();
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
3975
|
// #750 — `"user.userId"` is shorthand for `{ value: "user.userId", on:
|
|
3989
3976
|
// ["create"] }`, and a scalar `on` for a one-element list.
|
|
3990
3977
|
const auto = typeConfig.autoPopulatedFields;
|
|
@@ -4031,70 +4018,15 @@ function canonicalizeDatabaseTypeConfig(typeConfig) {
|
|
|
4031
4018
|
}
|
|
4032
4019
|
}
|
|
4033
4020
|
}
|
|
4034
|
-
/**
|
|
4035
|
-
* What this push's operation set will BE, stated to the server (issues #813,
|
|
4036
|
-
* #1336, #2732).
|
|
4037
|
-
*
|
|
4038
|
-
* The type-config PATCH runs its gates BEFORE the same push's operation calls,
|
|
4039
|
-
* so without a look-ahead every gate judges the pre-push operations: a push
|
|
4040
|
-
* that removes a model is blocked by the very references it is deleting. The
|
|
4041
|
-
* file IS the target state (config-as-code), so the claims are derived from it:
|
|
4042
|
-
*
|
|
4043
|
-
* - `finalOpNames` — the names the type ends with. Both other claims are
|
|
4044
|
-
* verified against it server-side, which is what keeps them from being a
|
|
4045
|
-
* gate bypass for a direct-API caller.
|
|
4046
|
-
* - `pendingOpDeletes` — its complement among the ops last sync recorded, so
|
|
4047
|
-
* an op this push deletes is excluded from the schema / OPS_EXIST gates.
|
|
4048
|
-
* - `pendingOpUpserts` — the post-push BODY of every declared op, so the
|
|
4049
|
-
* schema-edit gate lints a rewritten op as rewritten and a model removal
|
|
4050
|
-
* plus its operation rewrites lands in one push (#2732).
|
|
4051
|
-
* - `pendingOpUpdates` — the rule-only form (`access`/`params`) the manifest
|
|
4052
|
-
* re-lint reads (#1336).
|
|
4053
|
-
*
|
|
4054
|
-
* All four are built here and sent together, on the dry-run and on the real
|
|
4055
|
-
* PATCH alike. They are never conditioned on the push having a deletion: a push
|
|
4056
|
-
* that only REWRITES operations is exactly the case #2732 exists for.
|
|
4057
|
-
*/
|
|
4058
|
-
export function buildPendingOpClaims(operations, existingOpNames) {
|
|
4059
|
-
const finalOpNames = operations.map((op) => op.name);
|
|
4060
|
-
const declared = new Set(finalOpNames);
|
|
4061
|
-
return {
|
|
4062
|
-
pendingOpDeletes: existingOpNames.filter((n) => !declared.has(n)),
|
|
4063
|
-
finalOpNames,
|
|
4064
|
-
pendingOpUpdates: operations.map((op) => ({
|
|
4065
|
-
name: op.name,
|
|
4066
|
-
access: typeof op.access === "string" ? op.access : null,
|
|
4067
|
-
params: op.params ?? null,
|
|
4068
|
-
})),
|
|
4069
|
-
// The same body the op create/update call sends, so the gate lints what
|
|
4070
|
-
// this push is about to store.
|
|
4071
|
-
pendingOpUpserts: operations.map((op) => ({
|
|
4072
|
-
name: op.name,
|
|
4073
|
-
type: op.type,
|
|
4074
|
-
modelName: op.modelName,
|
|
4075
|
-
access: op.access,
|
|
4076
|
-
definition: op.definition,
|
|
4077
|
-
params: op.params,
|
|
4078
|
-
})),
|
|
4079
|
-
};
|
|
4080
|
-
}
|
|
4081
4021
|
export function parseDatabaseTypeToml(tomlData) {
|
|
4082
4022
|
const typeSection = tomlData.type || {};
|
|
4083
4023
|
// #2644 — the `[type]` field list comes from the vendored definition, so a
|
|
4084
4024
|
// scalar added there is read off the file with no edit here. The values the
|
|
4085
4025
|
// handler rewrites before storing — `autoPopulatedFields` (#750), its
|
|
4086
|
-
// `timestamps` sibling (#748)
|
|
4026
|
+
// `timestamps` sibling (#748) — are canonicalized
|
|
4087
4027
|
// below so the comparison reads the spelling the server keeps (#2731 B1).
|
|
4088
4028
|
const typeConfig = dropUndefined(buildPayloadFromToml(DATABASE_TYPE_TABLE, typeSection, {
|
|
4089
4029
|
mode: "create",
|
|
4090
|
-
values: {
|
|
4091
|
-
// Accept both `celContextAccess` (new user-facing key, the definition's
|
|
4092
|
-
// tomlKey) and `metadataAccess` (legacy, declared in `tomlOnlyKeys`).
|
|
4093
|
-
// The newer key wins when both appear. Issue #1567: a defined-check,
|
|
4094
|
-
// not a truthy guard, so `celContextAccess = ""` is honored as an
|
|
4095
|
-
// explicit clear rather than falling through to the legacy key.
|
|
4096
|
-
metadataAccess: (value, _mode, section) => value !== undefined ? value : section?.metadataAccess,
|
|
4097
|
-
},
|
|
4098
4030
|
}));
|
|
4099
4031
|
canonicalizeDatabaseTypeConfig(typeConfig);
|
|
4100
4032
|
// The `ruleSetName` reference; it resolves to an id later.
|
|
@@ -4133,27 +4065,11 @@ export function parseDatabaseTypeToml(tomlData) {
|
|
|
4133
4065
|
}
|
|
4134
4066
|
}
|
|
4135
4067
|
typeConfig.schema = schemaSerialized;
|
|
4136
|
-
// `
|
|
4137
|
-
//
|
|
4138
|
-
//
|
|
4139
|
-
//
|
|
4140
|
-
|
|
4141
|
-
const normalized = normalizeOperationFromToml(op);
|
|
4142
|
-
// #2731 B1 — the handler trims `name` before storing, and the trimmed name
|
|
4143
|
-
// is half the row's key (`<databaseType>#<name>`). Comparing or sending the
|
|
4144
|
-
// padded spelling names a row the server never holds: the child reconcile
|
|
4145
|
-
// would read it as a different operation and plan it forever.
|
|
4146
|
-
if (typeof normalized.name === "string") {
|
|
4147
|
-
normalized.name = normalized.name.trim();
|
|
4148
|
-
}
|
|
4149
|
-
return normalized;
|
|
4150
|
-
});
|
|
4151
|
-
// Subscriptions (issue #803): `[[subscriptions]]` array-of-tables.
|
|
4152
|
-
// `normalizeSubscriptionFromToml` maps `accessRule`→`access` (throwing on a
|
|
4153
|
-
// conflicting both-present declaration) and forwards `select`/`emit`/`params`
|
|
4154
|
-
// in the wire shape. Until #803 this block was dropped at parse time.
|
|
4155
|
-
const subscriptions = (tomlData.subscriptions || []).map((sub) => normalizeSubscriptionFromToml(sub));
|
|
4156
|
-
return { typeConfig, operations, subscriptions };
|
|
4068
|
+
// #3189 — `[[operations]]` and `[[subscriptions]]` retired with database
|
|
4069
|
+
// operations and type-scoped subscriptions. The preflight refuses a file
|
|
4070
|
+
// that still carries either (`RETIRED_DOCUMENT_KEYS`), so nothing here reads
|
|
4071
|
+
// them.
|
|
4072
|
+
return { typeConfig };
|
|
4157
4073
|
}
|
|
4158
4074
|
/**
|
|
4159
4075
|
* The parsed shape of a rule-set TOML: the definition's fields plus the
|
|
@@ -4981,29 +4897,17 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4981
4897
|
"database-type-config": {
|
|
4982
4898
|
label: "database-type-config",
|
|
4983
4899
|
table: DATABASE_TYPE_TABLE,
|
|
4984
|
-
parse: (doc) =>
|
|
4985
|
-
|
|
4986
|
-
// The three sub-trees ride ON the type config, so they travel with it and
|
|
4987
|
-
// are pulled back out as extras below.
|
|
4988
|
-
typeConfig.__operations = operations;
|
|
4989
|
-
typeConfig.__subscriptions = subscriptions;
|
|
4990
|
-
return typeConfig;
|
|
4991
|
-
},
|
|
4992
|
-
serialize: (record, maps, extra) => serializeDatabaseType(record, extra?.operations ?? [], maps.ruleSetIdToName, {
|
|
4993
|
-
defaultForm: "native",
|
|
4994
|
-
subscriptions: extra?.subscriptions ?? [],
|
|
4995
|
-
}),
|
|
4900
|
+
parse: (doc) => parseDatabaseTypeToml(doc).typeConfig,
|
|
4901
|
+
serialize: (record, maps) => serializeDatabaseType(record, maps.ruleSetIdToName),
|
|
4996
4902
|
extras: (entity) => ({
|
|
4997
4903
|
// Structural, per the definition: the `[models.*]` schema, the
|
|
4998
|
-
// `[triggers]` tree
|
|
4904
|
+
// `[triggers]` tree and the manifest. (#3189 — the operation and
|
|
4905
|
+
// subscription rows that used to ride beside them are retired.)
|
|
4999
4906
|
schema: entity.schema ?? null,
|
|
5000
4907
|
triggers: entity.triggers ?? null,
|
|
5001
4908
|
metadataManifest: entity.metadataManifest ?? null,
|
|
5002
|
-
operations: byKey(entity.__operations, "name"),
|
|
5003
|
-
subscriptions: byKey(entity.__subscriptions, "subscriptionKey"),
|
|
5004
4909
|
_unresolvedRuleSetName: entity._unresolvedRuleSetName,
|
|
5005
4910
|
}),
|
|
5006
|
-
childKeys: { operations: "name", subscriptions: "subscriptionKey" },
|
|
5007
4911
|
tomlFields: ["schema"],
|
|
5008
4912
|
ruleSetRef: true,
|
|
5009
4913
|
describe: (entity) => `database type "${entity?.databaseType}"`,
|
|
@@ -5342,36 +5246,6 @@ export function withDerivedIdentity(doc, tomlPath, identity) {
|
|
|
5342
5246
|
export function resolveExistingId(input) {
|
|
5343
5247
|
return input.liveOk ? input.liveId : input.manifestId;
|
|
5344
5248
|
}
|
|
5345
|
-
/**
|
|
5346
|
-
* Which CHILD rows a database type's push reconciles against (#2731 B3).
|
|
5347
|
-
*
|
|
5348
|
-
* `resolveExistingId` answers this for a whole entity; a database type's
|
|
5349
|
-
* operations and subscriptions are rows with the same question and the same
|
|
5350
|
-
* two possible answers. The manifest can name an operation the server no
|
|
5351
|
-
* longer has (the update PUTs a row that is gone) and be silent about one it
|
|
5352
|
-
* does have (the create POSTs a duplicate key) — both abort the push with a
|
|
5353
|
-
* server error, which is exactly the recovery `--force` is supposed to
|
|
5354
|
-
* perform.
|
|
5355
|
-
*
|
|
5356
|
-
* When the reconcile read the rows, they ARE the baseline: existence, the
|
|
5357
|
-
* timestamp each update guards with, and — as the complement of the file's own
|
|
5358
|
-
* list — which rows this push deletes. `undefined` means the read did not
|
|
5359
|
-
* happen (the degraded path, or a type live state says is absent), and the
|
|
5360
|
-
* manifest stays what it has always been.
|
|
5361
|
-
*/
|
|
5362
|
-
export function liveChildBaseline(live, manifestChildren) {
|
|
5363
|
-
if (!live)
|
|
5364
|
-
return undefined;
|
|
5365
|
-
const baseline = {};
|
|
5366
|
-
for (const [key, row] of live) {
|
|
5367
|
-
baseline[key] = {
|
|
5368
|
-
// The live timestamp is the one an update guards with; the manifest's is
|
|
5369
|
-
// a stand-in only for a row the listing returned without one.
|
|
5370
|
-
modifiedAt: row?.modifiedAt || manifestChildren?.[key]?.modifiedAt || "",
|
|
5371
|
-
};
|
|
5372
|
-
}
|
|
5373
|
-
return baseline;
|
|
5374
|
-
}
|
|
5375
5249
|
/**
|
|
5376
5250
|
* The app's live rule sets, indexed the three ways push reads them (#2731 B3).
|
|
5377
5251
|
*
|
|
@@ -8315,30 +8189,10 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8315
8189
|
const metadataCategoryConfigsResult = metadataCategoryConfigsOutcome.ok
|
|
8316
8190
|
? metadataCategoryConfigsOutcome.value
|
|
8317
8191
|
: [];
|
|
8318
|
-
//
|
|
8319
|
-
// subscriptions
|
|
8320
|
-
//
|
|
8321
|
-
|
|
8322
|
-
//
|
|
8323
|
-
// A failed operation/subscription fetch also disqualifies the type from
|
|
8324
|
-
// pruning (#1659): the file we write for it is then incomplete, so the
|
|
8325
|
-
// pull's picture of that type is not one to reconcile against.
|
|
8326
|
-
let databaseTypeDetailsOk = true;
|
|
8327
|
-
const databaseTypesWithOps = await Promise.all(selectPull("database-type-config", Array.isArray(databaseTypeConfigsResult) ? databaseTypeConfigsResult : [], (t) => t.databaseType).map(async (typeConfig) => {
|
|
8328
|
-
const [opsOutcome, subsOutcome] = await Promise.all([
|
|
8329
|
-
attemptFetch(() => client.listDatabaseTypeOperations(resolvedAppId, typeConfig.databaseType)),
|
|
8330
|
-
attemptFetch(() => client.listDatabaseTypeSubscriptions(resolvedAppId, typeConfig.databaseType)),
|
|
8331
|
-
]);
|
|
8332
|
-
if (!opsOutcome.ok || !subsOutcome.ok)
|
|
8333
|
-
databaseTypeDetailsOk = false;
|
|
8334
|
-
const ops = opsOutcome.ok ? opsOutcome.value : [];
|
|
8335
|
-
const subs = subsOutcome.ok ? subsOutcome.value : [];
|
|
8336
|
-
return {
|
|
8337
|
-
typeConfig,
|
|
8338
|
-
operations: Array.isArray(ops) ? ops : [],
|
|
8339
|
-
subscriptions: Array.isArray(subs) ? subs : [],
|
|
8340
|
-
};
|
|
8341
|
-
}));
|
|
8192
|
+
// #3189 — a database type's file is its type config alone: the
|
|
8193
|
+
// operations and subscriptions that used to be fetched beside it are
|
|
8194
|
+
// retired, so there is nothing more to read per type.
|
|
8195
|
+
const databaseTypesToWrite = selectPull("database-type-config", Array.isArray(databaseTypeConfigsResult) ? databaseTypeConfigsResult : [], (t) => t.databaseType);
|
|
8342
8196
|
// Snapshot the pre-pull sync tree BEFORE we touch any local file
|
|
8343
8197
|
// (issue #578, Phase 1). All server data has been fetched and
|
|
8344
8198
|
// validated at this point — a failed pull already threw above, so we
|
|
@@ -8934,46 +8788,16 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8934
8788
|
const ruleSets = selectPull("rule-set", Array.isArray(ruleSetsResult) ? ruleSetsResult : [], (rs) => ruleSetFileKey(rs));
|
|
8935
8789
|
// Write database types
|
|
8936
8790
|
const databaseTypeEntities = {};
|
|
8937
|
-
for (const
|
|
8791
|
+
for (const typeConfig of databaseTypesToWrite) {
|
|
8938
8792
|
const filename = `${typeConfig.databaseType}.toml`;
|
|
8939
8793
|
const filePath = join(configDir, "database-type-configs", filename);
|
|
8940
|
-
|
|
8941
|
-
// file already exists, read it raw and detect which operations
|
|
8942
|
-
// currently use JSON-string vs native nested-table form.
|
|
8943
|
-
const hints = existsSync(filePath)
|
|
8944
|
-
? detectExistingOperationForms(readFileSync(filePath, "utf-8"))
|
|
8945
|
-
: undefined;
|
|
8946
|
-
const content = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
8947
|
-
hints,
|
|
8948
|
-
defaultForm: "native",
|
|
8794
|
+
const content = serializeDatabaseType(typeConfig, ruleSetIdToName, {
|
|
8949
8795
|
logger: (msg) => info(` ${msg}`),
|
|
8950
|
-
// Issue #803: emit `[[subscriptions]]` blocks so a pull → push
|
|
8951
|
-
// cycle round-trips subscriptions without a spurious diff.
|
|
8952
|
-
subscriptions,
|
|
8953
8796
|
});
|
|
8954
8797
|
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("database-type-config", content, filePath, pullDiffMaps));
|
|
8955
|
-
const opsEntities = {};
|
|
8956
|
-
for (const op of operations) {
|
|
8957
|
-
opsEntities[op.name] = {
|
|
8958
|
-
modifiedAt: op.modifiedAt || new Date().toISOString(),
|
|
8959
|
-
};
|
|
8960
|
-
}
|
|
8961
|
-
// Track per-subscription modifiedAt in SyncState (issue #803),
|
|
8962
|
-
// exactly like operations, so the next push skips unchanged
|
|
8963
|
-
// subscriptions and detects removals.
|
|
8964
|
-
const subsEntities = {};
|
|
8965
|
-
for (const sub of subscriptions || []) {
|
|
8966
|
-
if (!sub?.subscriptionKey)
|
|
8967
|
-
continue;
|
|
8968
|
-
subsEntities[sub.subscriptionKey] = {
|
|
8969
|
-
modifiedAt: sub.modifiedAt || new Date().toISOString(),
|
|
8970
|
-
};
|
|
8971
|
-
}
|
|
8972
8798
|
databaseTypeEntities[typeConfig.databaseType] = {
|
|
8973
8799
|
databaseType: typeConfig.databaseType,
|
|
8974
8800
|
modifiedAt: typeConfig.modifiedAt || new Date().toISOString(),
|
|
8975
|
-
operations: Object.keys(opsEntities).length > 0 ? opsEntities : undefined,
|
|
8976
|
-
subscriptions: Object.keys(subsEntities).length > 0 ? subsEntities : undefined,
|
|
8977
8801
|
contentHash: computeFileHash(filePath),
|
|
8978
8802
|
semanticHash: pulledSemanticHash("database-type-config", content, pullDiffMaps),
|
|
8979
8803
|
hasSchema: typeof typeConfig.schema === "string" &&
|
|
@@ -9098,8 +8922,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9098
8922
|
scripts: scriptPresence,
|
|
9099
8923
|
functions: functionPresence,
|
|
9100
8924
|
emailTemplates: emailTemplatePresence,
|
|
9101
|
-
databaseTypes: databaseTypeConfigsOutcome.ok
|
|
9102
|
-
? { ok: true, serverKeys:
|
|
8925
|
+
databaseTypes: databaseTypeConfigsOutcome.ok
|
|
8926
|
+
? { ok: true, serverKeys: databaseTypesToWrite.map((t) => t.databaseType) }
|
|
9103
8927
|
: { ok: false },
|
|
9104
8928
|
ruleSets: ruleSetsOutcome.ok
|
|
9105
8929
|
? { ok: true, serverKeys: ruleSets.map((rs) => ruleSetFileKey(rs)) }
|
|
@@ -9459,7 +9283,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9459
9283
|
if (unwrittenTestCases > 0) {
|
|
9460
9284
|
keyValue("Test Cases not written", unwrittenTestCases);
|
|
9461
9285
|
}
|
|
9462
|
-
keyValue("Database Types",
|
|
9286
|
+
keyValue("Database Types", databaseTypesToWrite.length);
|
|
9463
9287
|
keyValue("Rule Sets", ruleSets.length);
|
|
9464
9288
|
keyValue("Group Type Configs", groupTypeConfigs.length);
|
|
9465
9289
|
keyValue("Collection Type Configs", collectionTypeConfigs.length);
|
|
@@ -11781,16 +11605,6 @@ What a push guarantees (two stages):
|
|
|
11781
11605
|
catch (err) {
|
|
11782
11606
|
throw new Error(`${filePath}: ${err.message}`);
|
|
11783
11607
|
}
|
|
11784
|
-
// #2652 — a prompt with no rule denies every non-admin execution.
|
|
11785
|
-
// Warn per file so an operator pushing an unmigrated app can see
|
|
11786
|
-
// exactly which prompts are locked down and how to open them.
|
|
11787
|
-
const promptRuleWarning = missingAccessRuleWarning({
|
|
11788
|
-
kind: "prompt",
|
|
11789
|
-
key,
|
|
11790
|
-
accessRule: prompt.accessRule,
|
|
11791
|
-
});
|
|
11792
|
-
if (promptRuleWarning)
|
|
11793
|
-
warn(` ${promptRuleWarning}`);
|
|
11794
11608
|
const existingEntry = syncState?.entities?.prompts?.[key];
|
|
11795
11609
|
const livePromptId = livePromptIds.get(key);
|
|
11796
11610
|
// A detail read that fails degrades THIS prompt to the byte-hash
|
|
@@ -12805,7 +12619,6 @@ What a push guarantees (two stages):
|
|
|
12805
12619
|
// start from this release onward. A `runAs = "system"` workflow is
|
|
12806
12620
|
// exempt (it rejects rules and members cannot start it).
|
|
12807
12621
|
const workflowRuleWarning = missingAccessRuleWarning({
|
|
12808
|
-
kind: "workflow",
|
|
12809
12622
|
key,
|
|
12810
12623
|
accessRule: workflow.accessRule,
|
|
12811
12624
|
runAs: workflow.runAs,
|
|
@@ -13257,12 +13070,11 @@ What a push guarantees (two stages):
|
|
|
13257
13070
|
const dbTypesDir = join(configDir, "database-type-configs");
|
|
13258
13071
|
if (existsSync(dbTypesDir)) {
|
|
13259
13072
|
const files = selectFiles("database-type-config", dbTypesDir, readdirSync(dbTypesDir).filter((f) => f.endsWith(".toml")));
|
|
13260
|
-
// #2731 B3 — live reconcile for the FILE-LEVEL gate
|
|
13261
|
-
//
|
|
13262
|
-
//
|
|
13263
|
-
//
|
|
13264
|
-
//
|
|
13265
|
-
// id→name map, so a failed rule-set fetch degrades this type too.
|
|
13073
|
+
// #2731 B3 — live reconcile for the FILE-LEVEL gate. Like `config
|
|
13074
|
+
// diff`, the comparison also needs the rule-set id→name map, so a
|
|
13075
|
+
// failed rule-set fetch degrades this type too. (#3189 — a type's
|
|
13076
|
+
// file is its config alone; the operation and subscription rows the
|
|
13077
|
+
// gate used to read beside it are retired.)
|
|
13266
13078
|
const databaseTypeSpec = configDiffSpec("database-type-config");
|
|
13267
13079
|
const liveDatabaseTypes = new Map();
|
|
13268
13080
|
let databaseTypesLiveOk = (await loadLiveRuleSets()).ok;
|
|
@@ -13284,28 +13096,8 @@ What a push guarantees (two stages):
|
|
|
13284
13096
|
}
|
|
13285
13097
|
for (const file of files) {
|
|
13286
13098
|
const filePath = join(dbTypesDir, file);
|
|
13287
|
-
const rawToml = readFileSync(filePath, "utf-8");
|
|
13288
13099
|
const tomlData = parseTomlFile(filePath);
|
|
13289
|
-
|
|
13290
|
-
let operations;
|
|
13291
|
-
let subscriptions;
|
|
13292
|
-
try {
|
|
13293
|
-
({ typeConfig, operations, subscriptions } =
|
|
13294
|
-
parseDatabaseTypeToml(tomlData));
|
|
13295
|
-
}
|
|
13296
|
-
catch (err) {
|
|
13297
|
-
// The only structured parse error today is the
|
|
13298
|
-
// `accessRule`/`access` conflict in a `[[subscriptions]]` block
|
|
13299
|
-
// (issue #803). Surface it with the file name and abort the
|
|
13300
|
-
// push rather than crashing with an opaque stack.
|
|
13301
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
13302
|
-
// Issue #976: throw (not `process.exit`) so the save-on-failure
|
|
13303
|
-
// catch is the only exit path. Validation is also front-loaded
|
|
13304
|
-
// above, so this is defense-in-depth.
|
|
13305
|
-
throw new Error(` ${file}: ${err.message}\nAborting push: subscription \`access\`/\`accessRule\` conflict in ${file}.`);
|
|
13306
|
-
}
|
|
13307
|
-
throw err;
|
|
13308
|
-
}
|
|
13100
|
+
const { typeConfig } = parseDatabaseTypeToml(tomlData);
|
|
13309
13101
|
const dbType = typeConfig.databaseType || basename(file, ".toml");
|
|
13310
13102
|
// The gate re-parses this document, so the name-derived identity
|
|
13311
13103
|
// has to be in it (see `withDerivedIdentity`).
|
|
@@ -13314,63 +13106,21 @@ What a push guarantees (two stages):
|
|
|
13314
13106
|
resolveRuleSetReference(typeConfig, ruleSetNameToId, `database type ${dbType}`, {
|
|
13315
13107
|
hint: ruleSetSelectionHint,
|
|
13316
13108
|
});
|
|
13317
|
-
// $params validator (issue #752): every `$params.X` reference
|
|
13318
|
-
// inside `definition` must correspond to a declared param in
|
|
13319
|
-
// `[[operations.params]]`. Catches typos like `$params.proectId`
|
|
13320
|
-
// at push time with the TOML file:line of the offending op block.
|
|
13321
|
-
const validation = validateOperations({
|
|
13322
|
-
filePath,
|
|
13323
|
-
rawToml,
|
|
13324
|
-
operations,
|
|
13325
|
-
});
|
|
13326
|
-
for (const w of validation.warnings) {
|
|
13327
|
-
warn(` ${formatIssue(w)}`);
|
|
13328
|
-
}
|
|
13329
|
-
if (validation.errors.length > 0) {
|
|
13330
|
-
// Issue #976: throw (not `process.exit`) so the save-on-failure
|
|
13331
|
-
// catch is the only exit path. Validation is also front-loaded
|
|
13332
|
-
// above, so this is defense-in-depth.
|
|
13333
|
-
throw new Error(validation.errors.map((e) => ` ${formatIssue(e)}`).join("\n") +
|
|
13334
|
-
`\nAborting push: ${validation.errors.length} unresolved $params reference(s) in ${file}.`);
|
|
13335
|
-
}
|
|
13336
13109
|
const existingEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
13337
13110
|
/**
|
|
13338
|
-
* This type as the server holds it NOW
|
|
13339
|
-
* subscriptions, which the comparison treats as one unit.
|
|
13111
|
+
* This type as the server holds it NOW.
|
|
13340
13112
|
*
|
|
13341
13113
|
* Two callers: the baseline stamped after everything this file
|
|
13342
13114
|
* writes has landed (B4), and the report for a 409 that arrived
|
|
13343
13115
|
* after the reconcile fetch (B5).
|
|
13344
13116
|
*/
|
|
13345
13117
|
const refetchLiveDatabaseType = async () => {
|
|
13346
|
-
const
|
|
13347
|
-
|
|
13348
|
-
client.listDatabaseTypeOperations(resolvedAppId, dbType),
|
|
13349
|
-
client.listDatabaseTypeSubscriptions(resolvedAppId, dbType),
|
|
13350
|
-
]);
|
|
13351
|
-
return record
|
|
13352
|
-
? {
|
|
13353
|
-
record,
|
|
13354
|
-
extra: {
|
|
13355
|
-
operations: Array.isArray(ops) ? ops : [],
|
|
13356
|
-
subscriptions: Array.isArray(subs) ? subs : [],
|
|
13357
|
-
},
|
|
13358
|
-
}
|
|
13359
|
-
: null;
|
|
13118
|
+
const record = await client.getDatabaseTypeConfig(resolvedAppId, dbType);
|
|
13119
|
+
return record ? { record, extra: {} } : null;
|
|
13360
13120
|
};
|
|
13361
|
-
// Issue #915 (defect b): track whether the apply path rejected any
|
|
13362
|
-
// op for THIS file via the schema gate (OperationRefError /
|
|
13363
|
-
// SchemaRequiredError). On a non-transactional fresh-type push the
|
|
13364
|
-
// db-type lands but the bad op is blocked and the loop continues; if
|
|
13365
|
-
// we then wrote the file-level contentHash, the next push's
|
|
13366
|
-
// `shouldPushFile` would short-circuit and the missing op would be
|
|
13367
|
-
// invisible drift. When this flag is set we skip the contentHash
|
|
13368
|
-
// write so the drift stays visible and a corrected re-push converges.
|
|
13369
|
-
let fileHadGateRejectedOp = false;
|
|
13370
13121
|
// #2731 B4: a 409 means the write was REJECTED, so this file did
|
|
13371
|
-
// not land
|
|
13372
|
-
//
|
|
13373
|
-
// refused — a baseline that lied would make the next push read the
|
|
13122
|
+
// not land. Tracked so the baseline stamp below cannot claim
|
|
13123
|
+
// convergence for content the server refused — a baseline that lied would make the next push read the
|
|
13374
13124
|
// still-unapplied local edit as server drift and refuse to retry.
|
|
13375
13125
|
let fileHadConflict = false;
|
|
13376
13126
|
const liveDatabaseType = databaseTypesLiveOk
|
|
@@ -13383,9 +13133,6 @@ What a push guarantees (two stages):
|
|
|
13383
13133
|
const typeExistsOnServer = databaseTypesLiveOk
|
|
13384
13134
|
? !!liveDatabaseType
|
|
13385
13135
|
: !!existingEntry;
|
|
13386
|
-
/** Live operations/subscriptions, keyed by name, when read. */
|
|
13387
|
-
let liveOpsByName;
|
|
13388
|
-
let liveSubsByName;
|
|
13389
13136
|
let dbTypeGate;
|
|
13390
13137
|
if (databaseTypesLiveOk && !liveDatabaseType) {
|
|
13391
13138
|
// The listing succeeded and this type is not in it. That is a
|
|
@@ -13394,87 +13141,47 @@ What a push guarantees (two stages):
|
|
|
13394
13141
|
dbTypeGate = { action: "create" };
|
|
13395
13142
|
}
|
|
13396
13143
|
else if (databaseTypesLiveOk && liveDatabaseType) {
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
.
|
|
13418
|
-
|
|
13419
|
-
dbTypeGate = decidePushForConfig({
|
|
13420
|
-
spec: databaseTypeSpec,
|
|
13421
|
-
maps: pushDiffMaps,
|
|
13422
|
-
localParsed: tomlData,
|
|
13423
|
-
live: {
|
|
13424
|
-
record: liveDatabaseType,
|
|
13425
|
-
extra: {
|
|
13426
|
-
operations: Array.isArray(liveOps) ? liveOps : [],
|
|
13427
|
-
subscriptions: Array.isArray(liveSubs) ? liveSubs : [],
|
|
13428
|
-
},
|
|
13429
|
-
modifiedAt: liveDatabaseType.modifiedAt,
|
|
13430
|
-
},
|
|
13431
|
-
entry: existingEntry,
|
|
13432
|
-
localFileHash: computeFileHash(filePath),
|
|
13433
|
-
force: options.force,
|
|
13434
|
-
});
|
|
13435
|
-
if (dbTypeGate.action === "skip") {
|
|
13436
|
-
skippedCount++;
|
|
13437
|
-
if (syncState) {
|
|
13438
|
-
// Written even when the manifest has never seen this type:
|
|
13439
|
-
// an adopt that records nothing is repeated on every push,
|
|
13440
|
-
// and its next local edit would have no baseline (and no
|
|
13441
|
-
// op/subscription rows) to reconcile against.
|
|
13442
|
-
if (!syncState.entities.databaseTypes) {
|
|
13443
|
-
syncState.entities.databaseTypes = {};
|
|
13444
|
-
}
|
|
13445
|
-
const opsEntities = {};
|
|
13446
|
-
for (const [opName, op] of liveOpsByName ?? []) {
|
|
13447
|
-
opsEntities[opName] = {
|
|
13448
|
-
modifiedAt: op?.modifiedAt || new Date().toISOString(),
|
|
13449
|
-
};
|
|
13450
|
-
}
|
|
13451
|
-
const subsEntities = {};
|
|
13452
|
-
for (const [subKey, sub] of liveSubsByName ?? []) {
|
|
13453
|
-
subsEntities[subKey] = {
|
|
13454
|
-
modifiedAt: sub?.modifiedAt || new Date().toISOString(),
|
|
13455
|
-
};
|
|
13456
|
-
}
|
|
13457
|
-
syncState.entities.databaseTypes[dbType] = {
|
|
13458
|
-
databaseType: dbType,
|
|
13459
|
-
modifiedAt: liveDatabaseType.modifiedAt ||
|
|
13460
|
-
existingEntry?.modifiedAt ||
|
|
13461
|
-
new Date().toISOString(),
|
|
13462
|
-
operations: Object.keys(opsEntities).length > 0 ? opsEntities : undefined,
|
|
13463
|
-
subscriptions: Object.keys(subsEntities).length > 0 ? subsEntities : undefined,
|
|
13464
|
-
contentHash: computeFileHash(filePath),
|
|
13465
|
-
semanticHash: dbTypeGate.remoteHash,
|
|
13466
|
-
hasSchema: typeof liveDatabaseType.schema === "string" &&
|
|
13467
|
-
liveDatabaseType.schema.trim().length > 0,
|
|
13468
|
-
};
|
|
13144
|
+
dbTypeGate = decidePushForConfig({
|
|
13145
|
+
spec: databaseTypeSpec,
|
|
13146
|
+
maps: pushDiffMaps,
|
|
13147
|
+
localParsed: tomlData,
|
|
13148
|
+
live: {
|
|
13149
|
+
record: liveDatabaseType,
|
|
13150
|
+
extra: {},
|
|
13151
|
+
modifiedAt: liveDatabaseType.modifiedAt,
|
|
13152
|
+
},
|
|
13153
|
+
entry: existingEntry,
|
|
13154
|
+
localFileHash: computeFileHash(filePath),
|
|
13155
|
+
force: options.force,
|
|
13156
|
+
});
|
|
13157
|
+
if (dbTypeGate.action === "skip") {
|
|
13158
|
+
skippedCount++;
|
|
13159
|
+
if (syncState) {
|
|
13160
|
+
// Written even when the manifest has never seen this type:
|
|
13161
|
+
// an adopt that records nothing is repeated on every push,
|
|
13162
|
+
// and its next local edit would have no baseline to
|
|
13163
|
+
// reconcile against.
|
|
13164
|
+
if (!syncState.entities.databaseTypes) {
|
|
13165
|
+
syncState.entities.databaseTypes = {};
|
|
13469
13166
|
}
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13167
|
+
syncState.entities.databaseTypes[dbType] = {
|
|
13168
|
+
databaseType: dbType,
|
|
13169
|
+
modifiedAt: liveDatabaseType.modifiedAt ||
|
|
13170
|
+
existingEntry?.modifiedAt ||
|
|
13171
|
+
new Date().toISOString(),
|
|
13172
|
+
contentHash: computeFileHash(filePath),
|
|
13173
|
+
semanticHash: dbTypeGate.remoteHash,
|
|
13174
|
+
hasSchema: typeof liveDatabaseType.schema === "string" &&
|
|
13175
|
+
liveDatabaseType.schema.trim().length > 0,
|
|
13176
|
+
};
|
|
13477
13177
|
}
|
|
13178
|
+
continue;
|
|
13179
|
+
}
|
|
13180
|
+
if (dbTypeGate.action === "drift" ||
|
|
13181
|
+
dbTypeGate.action === "conflict" ||
|
|
13182
|
+
dbTypeGate.action === "immutable") {
|
|
13183
|
+
recordDeclined("database-type-config", dbType, dbTypeGate, existingEntry?.modifiedAt);
|
|
13184
|
+
continue;
|
|
13478
13185
|
}
|
|
13479
13186
|
}
|
|
13480
13187
|
// Fallback gate: the manifest byte hash — and, since #2880, a
|
|
@@ -13494,26 +13201,6 @@ What a push guarantees (two stages):
|
|
|
13494
13201
|
continue;
|
|
13495
13202
|
}
|
|
13496
13203
|
}
|
|
13497
|
-
// ── Issues #813 / #1336 / #2732: state what this push's operation
|
|
13498
|
-
// set will BE, then run a validate-first pass before any mutating
|
|
13499
|
-
// call for this type. The type-config PATCH gates run ahead of the
|
|
13500
|
-
// operation calls, so the claims are what let them judge the
|
|
13501
|
-
// post-push state (see `buildPendingOpClaims`). ──
|
|
13502
|
-
// The op rows the server holds (#2731). When the reconcile read
|
|
13503
|
-
// them, they ARE the inventory (`liveChildBaseline`): an op created
|
|
13504
|
-
// out-of-band is UPDATEd rather than POSTed into a duplicate-key
|
|
13505
|
-
// failure, and one deleted out-of-band is re-CREATEd rather than
|
|
13506
|
-
// PATCHed at a row that is gone — and the claims' delete complement
|
|
13507
|
-
// is computed against that live inventory, not the manifest's. When
|
|
13508
|
-
// live state says the TYPE is gone, its ops went with it — so the
|
|
13509
|
-
// manifest's op list is not a baseline either, and this push
|
|
13510
|
-
// creates them alongside the re-created type. Only the degraded
|
|
13511
|
-
// path still reads the manifest, where it is what the byte-hash
|
|
13512
|
-
// gate has always used.
|
|
13513
|
-
const existingOpsForType = liveChildBaseline(liveOpsByName, existingEntry?.operations) ??
|
|
13514
|
-
((typeExistsOnServer ? existingEntry?.operations : undefined) || {});
|
|
13515
|
-
const pendingOpClaims = buildPendingOpClaims(operations, Object.keys(existingOpsForType));
|
|
13516
|
-
const { pendingOpDeletes } = pendingOpClaims;
|
|
13517
13204
|
// Whether the SERVER holds a schema right now. Live state answers
|
|
13518
13205
|
// when it was read (so an adopted type's schema is not silently
|
|
13519
13206
|
// preserved by an omitted key); the manifest's `hasSchema` is the
|
|
@@ -13572,8 +13259,9 @@ What a push guarantees (two stages):
|
|
|
13572
13259
|
// declares an owned scalar (matches the pre-#1567 PATCH trigger), a
|
|
13573
13260
|
// schema set/delete is in the payload, or an owned scalar is being
|
|
13574
13261
|
// cleared. When false, the type-config PATCH is skipped entirely so
|
|
13575
|
-
// an
|
|
13576
|
-
// "Updated" (issue #369 regression) and no idle
|
|
13262
|
+
// an edit with no type-level effect does not spuriously report the
|
|
13263
|
+
// type as "Updated" (issue #369 regression) and no idle
|
|
13264
|
+
// clear-report fires.
|
|
13577
13265
|
const declaresOwnedField = DB_TYPE_OWNED_SCALARS.some((k) => k in typeConfig);
|
|
13578
13266
|
const schemaInUpdateData = "schema" in computeTypeUpdateData();
|
|
13579
13267
|
const typeLevelChange = declaresOwnedField ||
|
|
@@ -13585,19 +13273,12 @@ What a push guarantees (two stages):
|
|
|
13585
13273
|
info(` ${prefix}${dbTypeFieldLabel(key)} on ${dbType}`);
|
|
13586
13274
|
}
|
|
13587
13275
|
};
|
|
13588
|
-
// Validate-first pass (decision 4a): run
|
|
13589
|
-
//
|
|
13590
|
-
//
|
|
13591
|
-
//
|
|
13592
|
-
//
|
|
13593
|
-
// it runs the real server gates without persisting (Option 1A).
|
|
13276
|
+
// Validate-first pass (decision 4a): run the type's gates BEFORE
|
|
13277
|
+
// issuing any mutating call. If a gate fails, the type's mutating
|
|
13278
|
+
// calls are skipped. The same pass IS the dry-run path for
|
|
13279
|
+
// `config push --dry-run`: it runs the real server gates without
|
|
13280
|
+
// persisting (Option 1A).
|
|
13594
13281
|
let typeGateBlocked = false;
|
|
13595
|
-
// Issue #684 regression guard: track which op the op-edit gate is
|
|
13596
|
-
// dry-running so an unrecognized server error (e.g. a plain
|
|
13597
|
-
// validation 400 like `"limit" must be 1000 or less`) can be
|
|
13598
|
-
// attributed to that specific operation in the catch-all below,
|
|
13599
|
-
// instead of being coarsened to the db-type level.
|
|
13600
|
-
let opGateContext = null;
|
|
13601
13282
|
// #2731 B4 — the concurrency guard is the LIVE timestamp the
|
|
13602
13283
|
// reconcile fetch just read (the gate carries it), not the
|
|
13603
13284
|
// manifest's, which can be arbitrarily stale and 409 on a write
|
|
@@ -13609,54 +13290,28 @@ What a push guarantees (two stages):
|
|
|
13609
13290
|
const validateExpectedModifiedAt = options.force
|
|
13610
13291
|
? undefined
|
|
13611
13292
|
: dbTypeExpectedModifiedAt;
|
|
13612
|
-
// Issue #1336 (codex round-1 P1/P2): the declared-access manifest
|
|
13613
|
-
// this push proposes (the parsed object, or null when the type
|
|
13614
|
-
// declares none / is clearing it). Forwarded as
|
|
13615
|
-
// `metadataManifestOverride` on the op dry-runs so op-access lints
|
|
13616
|
-
// run against the manifest the same push is landing, not the stale
|
|
13617
|
-
// stored one (existing type: a newly-declared key is honored; fresh
|
|
13618
|
-
// type: an undeclared ref is caught instead of skipped).
|
|
13619
|
-
const proposedManifest = typeConfig.metadataManifest ??
|
|
13620
|
-
null;
|
|
13621
13293
|
try {
|
|
13622
|
-
//
|
|
13623
|
-
//
|
|
13624
|
-
//
|
|
13625
|
-
//
|
|
13626
|
-
//
|
|
13627
|
-
// the server no longer has dry-runs its CREATE, not a PATCH.
|
|
13628
|
-
// Issue #1567: `computeTypeUpdateData` now always returns the
|
|
13629
|
-
// owned scalars, so gate on `typeLevelChange` (a genuine
|
|
13630
|
-
// declared/schema/clear change) rather than "payload
|
|
13631
|
-
// non-empty" — an operations-only edit must not run the
|
|
13632
|
-
// type-config dry-run at all.
|
|
13294
|
+
// The type-config PATCH dry-run — only when the type already
|
|
13295
|
+
// exists on the server and we'd actually PATCH a type-level
|
|
13296
|
+
// field. #2731 B3: "already exists" is live state's answer, so a
|
|
13297
|
+
// type the manifest still names but the server no longer has
|
|
13298
|
+
// dry-runs its CREATE, not a PATCH.
|
|
13633
13299
|
if (typeExistsOnServer && typeLevelChange) {
|
|
13634
|
-
|
|
13635
|
-
await client.updateDatabaseTypeConfig(resolvedAppId, dbType,
|
|
13636
|
-
// Forward the whole look-ahead (#813 / #1336 / #2732) so the
|
|
13637
|
-
// dry-run gates judge the post-push operation set — the same
|
|
13638
|
-
// payload the mutating PATCH below sends.
|
|
13639
|
-
{ ...validateUpdateData, ...pendingOpClaims }, validateExpectedModifiedAt, {
|
|
13300
|
+
await client.updateDatabaseTypeConfig(resolvedAppId, dbType, computeTypeUpdateData(), validateExpectedModifiedAt, {
|
|
13640
13301
|
dryRun: true,
|
|
13641
13302
|
acceptWarnings: !!options.acceptWarnings,
|
|
13642
13303
|
});
|
|
13643
13304
|
}
|
|
13644
13305
|
else if (!typeExistsOnServer) {
|
|
13645
13306
|
// Fresh type: run the config CREATE dry-run (issue #1336, codex
|
|
13646
|
-
// round-1 P2)
|
|
13647
|
-
//
|
|
13648
|
-
//
|
|
13649
|
-
// rule reported a clean `config push --dry-run` then failed only on
|
|
13650
|
-
// the real POST. Mirror the apply-path create payload below.
|
|
13307
|
+
// round-1 P2), so a malformed inline rule (autoPopulatedFields,
|
|
13308
|
+
// triggers) or manifest fails `config push --dry-run` as the
|
|
13309
|
+
// real POST would. Mirrors the apply-path create payload below.
|
|
13651
13310
|
const createDryRunData = { databaseType: dbType };
|
|
13652
13311
|
if (typeConfig.ruleSetId)
|
|
13653
13312
|
createDryRunData.ruleSetId = typeConfig.ruleSetId;
|
|
13654
13313
|
if (typeConfig.triggers)
|
|
13655
13314
|
createDryRunData.triggers = typeConfig.triggers;
|
|
13656
|
-
if (typeConfig.metadataAccess)
|
|
13657
|
-
createDryRunData.metadataAccess = typeConfig.metadataAccess;
|
|
13658
|
-
if (typeConfig.defaultAccess)
|
|
13659
|
-
createDryRunData.defaultAccess = typeConfig.defaultAccess;
|
|
13660
13315
|
if (typeConfig.autoPopulatedFields)
|
|
13661
13316
|
createDryRunData.autoPopulatedFields =
|
|
13662
13317
|
typeConfig.autoPopulatedFields;
|
|
@@ -13679,175 +13334,12 @@ What a push guarantees (two stages):
|
|
|
13679
13334
|
throw createErr;
|
|
13680
13335
|
}
|
|
13681
13336
|
}
|
|
13682
|
-
// 2. Op-edit gate (op create/update dry-run) for every op in the
|
|
13683
|
-
// TOML. The server runs `runSchemaGate` and short-circuits
|
|
13684
|
-
// before persisting.
|
|
13685
|
-
//
|
|
13686
|
-
// The op gate is run against the schema THIS push is landing
|
|
13687
|
-
// (`schemaOverride`), not the stale stored schema — the real
|
|
13688
|
-
// apply path PATCHes the schema before creating/updating ops,
|
|
13689
|
-
// so an op that depends on a field this push adds must NOT be
|
|
13690
|
-
// falsely rejected (plan edge case: "op create that depends on
|
|
13691
|
-
// a schema landed earlier in the same push"). When the push
|
|
13692
|
-
// isn't changing the schema, leave the override unset so the
|
|
13693
|
-
// server uses the stored schema.
|
|
13694
|
-
//
|
|
13695
|
-
// Issue #915 (defect a): this gate now runs for FRESH types too
|
|
13696
|
-
// (previously guarded by `if (existingEntry)`). For a brand-new
|
|
13697
|
-
// db-type the server has no stored config, so we gate the op
|
|
13698
|
-
// against the PROPOSED local schema via `schemaOverride`. The
|
|
13699
|
-
// server's op-create dry-run accepts a missing type when a
|
|
13700
|
-
// `schemaOverride` is present (see
|
|
13701
|
-
// database-type-operations-controller.ts). A fresh schemaless
|
|
13702
|
-
// type (#666 bootstrap) sends an empty/absent override, and
|
|
13703
|
-
// `runSchemaGate` is a no-op without a schema — so that path
|
|
13704
|
-
// still succeeds.
|
|
13705
|
-
{
|
|
13706
|
-
const localHasSchema = typeof typeConfig.schema === "string" &&
|
|
13707
|
-
typeConfig.schema.trim().length > 0;
|
|
13708
|
-
let proposedSchema;
|
|
13709
|
-
if (typeExistsOnServer) {
|
|
13710
|
-
const validateUpdateData = computeTypeUpdateData();
|
|
13711
|
-
proposedSchema =
|
|
13712
|
-
"schema" in validateUpdateData
|
|
13713
|
-
? validateUpdateData.schema
|
|
13714
|
-
: undefined;
|
|
13715
|
-
}
|
|
13716
|
-
else {
|
|
13717
|
-
// Fresh type: the proposed schema is whatever the local TOML
|
|
13718
|
-
// declares. A schemaless fresh type (no `[models.*]`) sends an
|
|
13719
|
-
// empty-string override so the server takes the no-schema =
|
|
13720
|
-
// no-op gate path rather than falling back to a (nonexistent)
|
|
13721
|
-
// stored config — keeping the #666 bootstrap convergent.
|
|
13722
|
-
proposedSchema = localHasSchema ? typeConfig.schema : "";
|
|
13723
|
-
}
|
|
13724
|
-
for (const op of operations) {
|
|
13725
|
-
const existingOp = existingOpsForType[op.name];
|
|
13726
|
-
opGateContext = {
|
|
13727
|
-
action: existingOp ? "update" : "create",
|
|
13728
|
-
key: `${dbType}/${op.name}`,
|
|
13729
|
-
};
|
|
13730
|
-
if (existingOp) {
|
|
13731
|
-
await client.updateDatabaseTypeOperation(resolvedAppId, dbType, op.name, {
|
|
13732
|
-
type: op.type,
|
|
13733
|
-
modelName: op.modelName,
|
|
13734
|
-
access: op.access,
|
|
13735
|
-
definition: op.definition,
|
|
13736
|
-
params: op.params,
|
|
13737
|
-
}, options.force ? undefined : existingOp.modifiedAt, {
|
|
13738
|
-
dryRun: true,
|
|
13739
|
-
schemaOverride: proposedSchema,
|
|
13740
|
-
// Issue #1336 (P1/P2): gate the op-access lint against
|
|
13741
|
-
// the manifest THIS push is landing, not the stale
|
|
13742
|
-
// stored one.
|
|
13743
|
-
metadataManifestOverride: proposedManifest,
|
|
13744
|
-
});
|
|
13745
|
-
}
|
|
13746
|
-
else {
|
|
13747
|
-
await client.createDatabaseTypeOperation(resolvedAppId, dbType, {
|
|
13748
|
-
name: op.name,
|
|
13749
|
-
type: op.type,
|
|
13750
|
-
modelName: op.modelName,
|
|
13751
|
-
access: op.access,
|
|
13752
|
-
definition: op.definition,
|
|
13753
|
-
params: op.params,
|
|
13754
|
-
}, {
|
|
13755
|
-
dryRun: true,
|
|
13756
|
-
schemaOverride: proposedSchema,
|
|
13757
|
-
// Issue #1336 (P1/P2): as above — a fresh-type op dry-run
|
|
13758
|
-
// now lints against the proposed manifest instead of
|
|
13759
|
-
// skipping the lint entirely.
|
|
13760
|
-
metadataManifestOverride: proposedManifest,
|
|
13761
|
-
// Issue #915 (defect a, follow-up): for a fresh type the
|
|
13762
|
-
// server has no stored config to read `defaultAccess`
|
|
13763
|
-
// from, so an op that omits `access` to inherit the
|
|
13764
|
-
// TOML-declared type-level `defaultAccess` would be
|
|
13765
|
-
// falsely rejected. Thread the proposed value so the
|
|
13766
|
-
// fresh-type dry-run gates the same way the real push
|
|
13767
|
-
// (which lands the type first) will.
|
|
13768
|
-
defaultAccess: typeof typeConfig.defaultAccess === "string"
|
|
13769
|
-
? typeConfig.defaultAccess
|
|
13770
|
-
: undefined,
|
|
13771
|
-
});
|
|
13772
|
-
}
|
|
13773
|
-
}
|
|
13774
|
-
// Every op gate passed; clear the context so a later (non-op)
|
|
13775
|
-
// throw inside this try isn't misattributed to the last op.
|
|
13776
|
-
opGateContext = null;
|
|
13777
|
-
}
|
|
13778
13337
|
}
|
|
13779
13338
|
catch (err) {
|
|
13780
|
-
// A gate failure during validate aborts the whole type.
|
|
13781
|
-
//
|
|
13782
|
-
//
|
|
13783
|
-
|
|
13784
|
-
// the apply path's handling (validate-first doesn't change
|
|
13785
|
-
// conflict semantics).
|
|
13786
|
-
if (err instanceof SchemaBreaksOpsError) {
|
|
13787
|
-
schemaErrors.schemaBreaks.push({
|
|
13788
|
-
type: "database-type-config",
|
|
13789
|
-
key: dbType,
|
|
13790
|
-
operations: err.operations,
|
|
13791
|
-
message: err.message,
|
|
13792
|
-
});
|
|
13793
|
-
blockedDatabaseTypes.push({
|
|
13794
|
-
databaseType: dbType,
|
|
13795
|
-
reason: `schema change blocked: ${err.operations.length} kept operation(s) would break under the new schema`,
|
|
13796
|
-
});
|
|
13797
|
-
typeGateBlocked = true;
|
|
13798
|
-
}
|
|
13799
|
-
else if (err instanceof SchemaHasUncheckableOpsError) {
|
|
13800
|
-
schemaErrors.uncheckableOps.push({
|
|
13801
|
-
type: "database-type-config",
|
|
13802
|
-
key: dbType,
|
|
13803
|
-
operations: err.operations,
|
|
13804
|
-
message: err.message,
|
|
13805
|
-
});
|
|
13806
|
-
blockedDatabaseTypes.push({
|
|
13807
|
-
databaseType: dbType,
|
|
13808
|
-
reason: `schema change blocked: operation(s) have dynamic refs (re-run with --accept-warnings to commit)`,
|
|
13809
|
-
});
|
|
13810
|
-
typeGateBlocked = true;
|
|
13811
|
-
}
|
|
13812
|
-
else if (err instanceof OpsExistError) {
|
|
13813
|
-
schemaErrors.opsExist.push({
|
|
13814
|
-
type: "database-type-config",
|
|
13815
|
-
key: dbType,
|
|
13816
|
-
opCount: err.opCount,
|
|
13817
|
-
message: err.message,
|
|
13818
|
-
});
|
|
13819
|
-
blockedDatabaseTypes.push({
|
|
13820
|
-
databaseType: dbType,
|
|
13821
|
-
reason: `schema deletion blocked: ${err.opCount} operation(s) still registered after this push's deletes`,
|
|
13822
|
-
});
|
|
13823
|
-
typeGateBlocked = true;
|
|
13824
|
-
}
|
|
13825
|
-
else if (err instanceof SchemaRequiredError) {
|
|
13826
|
-
schemaErrors.schemaRequired.push({
|
|
13827
|
-
type: "operation",
|
|
13828
|
-
key: `${dbType}/_`,
|
|
13829
|
-
message: err.message,
|
|
13830
|
-
});
|
|
13831
|
-
blockedDatabaseTypes.push({
|
|
13832
|
-
databaseType: dbType,
|
|
13833
|
-
reason: `operation push blocked: type has no schema set`,
|
|
13834
|
-
});
|
|
13835
|
-
typeGateBlocked = true;
|
|
13836
|
-
}
|
|
13837
|
-
else if (err instanceof OperationRefError) {
|
|
13838
|
-
schemaErrors.opRefs.push({
|
|
13839
|
-
type: "operation",
|
|
13840
|
-
key: dbType,
|
|
13841
|
-
refs: err.refs,
|
|
13842
|
-
message: err.message,
|
|
13843
|
-
});
|
|
13844
|
-
blockedDatabaseTypes.push({
|
|
13845
|
-
databaseType: dbType,
|
|
13846
|
-
reason: `operation push blocked: unresolved references in the schema`,
|
|
13847
|
-
});
|
|
13848
|
-
typeGateBlocked = true;
|
|
13849
|
-
}
|
|
13850
|
-
else if (err instanceof ConflictError) {
|
|
13339
|
+
// A gate failure during validate aborts the whole type. Conflicts
|
|
13340
|
+
// and other errors fall through to the apply path's handling
|
|
13341
|
+
// (validate-first doesn't change conflict semantics).
|
|
13342
|
+
if (err instanceof ConflictError) {
|
|
13851
13343
|
// Surface the conflict and skip this type (don't blow away the
|
|
13852
13344
|
// server with mutating calls). Mirrors the apply path.
|
|
13853
13345
|
conflicts.push({
|
|
@@ -13868,14 +13360,6 @@ What a push guarantees (two stages):
|
|
|
13868
13360
|
});
|
|
13869
13361
|
typeGateBlocked = true;
|
|
13870
13362
|
}
|
|
13871
|
-
else if (opGateContext) {
|
|
13872
|
-
// The op-edit gate dry-run threw a plain (non-gate) error —
|
|
13873
|
-
// e.g. an invalid op `definition` the server rejects with a
|
|
13874
|
-
// 400. The validate-first loop already knows which op failed,
|
|
13875
|
-
// so name it (issue #684) instead of coarsening to the
|
|
13876
|
-
// db-type level. Mirrors the apply path's per-op wraps.
|
|
13877
|
-
throw wrapEntityError(err, opGateContext.action, "operation", opGateContext.key);
|
|
13878
|
-
}
|
|
13879
13363
|
else {
|
|
13880
13364
|
throw wrapEntityError(err, "update", "database type", dbType);
|
|
13881
13365
|
}
|
|
@@ -13895,17 +13379,6 @@ What a push guarantees (two stages):
|
|
|
13895
13379
|
else {
|
|
13896
13380
|
changes.push({ type: "database-type-config", action: "create", key: dbType });
|
|
13897
13381
|
}
|
|
13898
|
-
for (const op of operations) {
|
|
13899
|
-
const existingOp = existingOpsForType[op.name];
|
|
13900
|
-
changes.push({
|
|
13901
|
-
type: "operation",
|
|
13902
|
-
action: existingOp ? "update" : "create",
|
|
13903
|
-
key: `${dbType}/${op.name}`,
|
|
13904
|
-
});
|
|
13905
|
-
}
|
|
13906
|
-
for (const delName of pendingOpDeletes) {
|
|
13907
|
-
changes.push({ type: "operation", action: "delete", key: `${dbType}/${delName}` });
|
|
13908
|
-
}
|
|
13909
13382
|
continue;
|
|
13910
13383
|
}
|
|
13911
13384
|
// A live type the manifest never recorded is about to be updated,
|
|
@@ -13931,9 +13404,8 @@ What a push guarantees (two stages):
|
|
|
13931
13404
|
// never recorded is updated rather than re-created.
|
|
13932
13405
|
if (typeExistsOnServer) {
|
|
13933
13406
|
// Update existing type config — only when there is a genuine
|
|
13934
|
-
// type-level change.
|
|
13935
|
-
//
|
|
13936
|
-
// sending an empty body (which the server rejects with HTTP 400)
|
|
13407
|
+
// type-level change. Issue #369: skipping the PATCH on an edit
|
|
13408
|
+
// with no type-level effect avoids sending an empty body (which the server rejects with HTTP 400)
|
|
13937
13409
|
// and a spurious "Updated database type". Issue #1567: the owned
|
|
13938
13410
|
// scalars are always in `updateData` now (value-or-`null`), so the
|
|
13939
13411
|
// skip decision moved from "payload non-empty" to the
|
|
@@ -13950,18 +13422,7 @@ What a push guarantees (two stages):
|
|
|
13950
13422
|
// `schema: null` clears it server-side; codex review gap on
|
|
13951
13423
|
// PR #766). Built by the shared helper so the validate pass
|
|
13952
13424
|
// and apply pass agree (issue #813).
|
|
13953
|
-
|
|
13954
|
-
// went on the dry-run above — the gates it feeds run on this
|
|
13955
|
-
// call, and the operation creates/updates/deletes it
|
|
13956
|
-
// describes still happen in step 3 below. It is sent whole
|
|
13957
|
-
// and unconditionally: a push that only rewrites operations
|
|
13958
|
-
// has no deletes to report, and gating on that was what kept
|
|
13959
|
-
// the #2732 case (model removal + operation rewrites) from
|
|
13960
|
-
// ever reaching the server as one push.
|
|
13961
|
-
const updateData = {
|
|
13962
|
-
...computeTypeUpdateData(),
|
|
13963
|
-
...pendingOpClaims,
|
|
13964
|
-
};
|
|
13425
|
+
const updateData = computeTypeUpdateData();
|
|
13965
13426
|
if (typeLevelChange) {
|
|
13966
13427
|
changes.push({ type: "database-type-config", action: "update", key: dbType });
|
|
13967
13428
|
const updated = await client.updateDatabaseTypeConfig(resolvedAppId, dbType, updateData, expectedModifiedAt, {
|
|
@@ -13970,8 +13431,8 @@ What a push guarantees (two stages):
|
|
|
13970
13431
|
});
|
|
13971
13432
|
info(` Updated database type: ${dbType}`);
|
|
13972
13433
|
// Issue #1567: name each owned scalar this push cleared, so
|
|
13973
|
-
// removing
|
|
13974
|
-
//
|
|
13434
|
+
// removing a rule (triggers, autoPopulatedFields) is never
|
|
13435
|
+
// silent.
|
|
13975
13436
|
reportClearedFields("Cleared ");
|
|
13976
13437
|
printStaleInstanceWarning(updated);
|
|
13977
13438
|
if (syncState?.entities?.databaseTypes?.[dbType] && updated?.modifiedAt) {
|
|
@@ -13988,28 +13449,11 @@ What a push guarantees (two stages):
|
|
|
13988
13449
|
if (err instanceof ConflictError) {
|
|
13989
13450
|
// #2731 B5 — the server moved between the reconcile fetch
|
|
13990
13451
|
// and this write, so recompute the field diff against the
|
|
13991
|
-
// record that is there NOW
|
|
13992
|
-
// subscriptions, which the comparison treats as one unit).
|
|
13452
|
+
// record that is there NOW.
|
|
13993
13453
|
conflicts.push(await lateConflict("database-type-config", dbType, err, expectedModifiedAt, refetchLiveDatabaseType, databaseTypeSpec, tomlData));
|
|
13994
13454
|
// The PATCH was rejected: this file did not land.
|
|
13995
13455
|
fileHadConflict = true;
|
|
13996
13456
|
}
|
|
13997
|
-
else if (err instanceof SchemaBreaksOpsError) {
|
|
13998
|
-
schemaErrors.schemaBreaks.push({
|
|
13999
|
-
type: "database-type-config",
|
|
14000
|
-
key: dbType,
|
|
14001
|
-
operations: err.operations,
|
|
14002
|
-
message: err.message,
|
|
14003
|
-
});
|
|
14004
|
-
}
|
|
14005
|
-
else if (err instanceof SchemaHasUncheckableOpsError) {
|
|
14006
|
-
schemaErrors.uncheckableOps.push({
|
|
14007
|
-
type: "database-type-config",
|
|
14008
|
-
key: dbType,
|
|
14009
|
-
operations: err.operations,
|
|
14010
|
-
message: err.message,
|
|
14011
|
-
});
|
|
14012
|
-
}
|
|
14013
13457
|
else if (err instanceof TomlParseError) {
|
|
14014
13458
|
schemaErrors.tomlParse.push({
|
|
14015
13459
|
type: "database-type-config",
|
|
@@ -14019,14 +13463,6 @@ What a push guarantees (two stages):
|
|
|
14019
13463
|
message: err.message,
|
|
14020
13464
|
});
|
|
14021
13465
|
}
|
|
14022
|
-
else if (err instanceof OpsExistError) {
|
|
14023
|
-
schemaErrors.opsExist.push({
|
|
14024
|
-
type: "database-type-config",
|
|
14025
|
-
key: dbType,
|
|
14026
|
-
opCount: err.opCount,
|
|
14027
|
-
message: err.message,
|
|
14028
|
-
});
|
|
14029
|
-
}
|
|
14030
13466
|
else {
|
|
14031
13467
|
throw wrapEntityError(err, "update", "database type", dbType);
|
|
14032
13468
|
}
|
|
@@ -14053,10 +13489,6 @@ What a push guarantees (two stages):
|
|
|
14053
13489
|
createData.ruleSetId = typeConfig.ruleSetId;
|
|
14054
13490
|
if (typeConfig.triggers)
|
|
14055
13491
|
createData.triggers = typeConfig.triggers;
|
|
14056
|
-
if (typeConfig.metadataAccess)
|
|
14057
|
-
createData.metadataAccess = typeConfig.metadataAccess;
|
|
14058
|
-
if (typeConfig.defaultAccess)
|
|
14059
|
-
createData.defaultAccess = typeConfig.defaultAccess;
|
|
14060
13492
|
if (typeConfig.autoPopulatedFields)
|
|
14061
13493
|
createData.autoPopulatedFields = typeConfig.autoPopulatedFields;
|
|
14062
13494
|
if (typeConfig.timestamps)
|
|
@@ -14124,11 +13556,7 @@ What a push guarantees (two stages):
|
|
|
14124
13556
|
// remote config and refuse to mutate it without a baseline:
|
|
14125
13557
|
// surface it as a SKIPPED type (reusing the #813 blocked-type
|
|
14126
13558
|
// disclosure + non-zero exit) so the user runs `config pull`
|
|
14127
|
-
// first.
|
|
14128
|
-
// operations are created against the un-reconciled type.
|
|
14129
|
-
const existingOpsRemote = await client
|
|
14130
|
-
.listDatabaseTypeOperations(resolvedAppId, dbType)
|
|
14131
|
-
.catch(() => []);
|
|
13559
|
+
// first.
|
|
14132
13560
|
const hasNonDefaultField = (v) => {
|
|
14133
13561
|
if (v === null || v === undefined)
|
|
14134
13562
|
return false;
|
|
@@ -14138,12 +13566,8 @@ What a push guarantees (two stages):
|
|
|
14138
13566
|
return Object.keys(v).length > 0;
|
|
14139
13567
|
return true;
|
|
14140
13568
|
};
|
|
14141
|
-
const isMinimalAutoCreated =
|
|
14142
|
-
existingOpsRemote.length === 0) &&
|
|
14143
|
-
!hasNonDefaultField(existing?.ruleSetId) &&
|
|
13569
|
+
const isMinimalAutoCreated = !hasNonDefaultField(existing?.ruleSetId) &&
|
|
14144
13570
|
!hasNonDefaultField(existing?.triggers) &&
|
|
14145
|
-
!hasNonDefaultField(existing?.metadataAccess) &&
|
|
14146
|
-
!hasNonDefaultField(existing?.defaultAccess) &&
|
|
14147
13571
|
!hasNonDefaultField(existing?.autoPopulatedFields) &&
|
|
14148
13572
|
!hasNonDefaultField(existing?.timestamps) &&
|
|
14149
13573
|
!hasNonDefaultField(existing?.schema);
|
|
@@ -14177,13 +13601,6 @@ What a push guarantees (two stages):
|
|
|
14177
13601
|
action: "update",
|
|
14178
13602
|
key: dbType,
|
|
14179
13603
|
});
|
|
14180
|
-
for (const op of operations) {
|
|
14181
|
-
changes.push({
|
|
14182
|
-
type: "operation",
|
|
14183
|
-
action: "create",
|
|
14184
|
-
key: `${dbType}/${op.name}`,
|
|
14185
|
-
});
|
|
14186
|
-
}
|
|
14187
13604
|
continue;
|
|
14188
13605
|
}
|
|
14189
13606
|
info(` reconciled auto-created database type \`${dbType}\``);
|
|
@@ -14208,7 +13625,7 @@ What a push guarantees (two stages):
|
|
|
14208
13625
|
// "modified since last sync" CONFLICT (Q4). Subsequent
|
|
14209
13626
|
// pushes carry the baseline just seeded above. Skip the
|
|
14210
13627
|
// PATCH entirely when the TOML declares no type-level
|
|
14211
|
-
// fields
|
|
13628
|
+
// fields so we don't send an empty
|
|
14212
13629
|
// body, which the server rejects with HTTP 400 (#369).
|
|
14213
13630
|
const reconcileData = computeTypeUpdateData();
|
|
14214
13631
|
if (Object.keys(reconcileData).length > 0) {
|
|
@@ -14227,11 +13644,6 @@ What a push guarantees (two stages):
|
|
|
14227
13644
|
: syncState.entities.databaseTypes[dbType].hasSchema;
|
|
14228
13645
|
}
|
|
14229
13646
|
}
|
|
14230
|
-
// Operations declared in the TOML flow through the existing
|
|
14231
|
-
// op sub-loop below. `existingOps` is derived from
|
|
14232
|
-
// `existingEntry` (the pre-push sync-state snapshot, which is
|
|
14233
|
-
// empty here), so every op is CREATEd — matching the empty
|
|
14234
|
-
// auto-created type on the server.
|
|
14235
13647
|
}
|
|
14236
13648
|
else {
|
|
14237
13649
|
throw wrapEntityError(err, "create", "database type", dbType);
|
|
@@ -14239,315 +13651,22 @@ What a push guarantees (two stages):
|
|
|
14239
13651
|
}
|
|
14240
13652
|
}
|
|
14241
13653
|
}
|
|
14242
|
-
//
|
|
14243
|
-
// the
|
|
14244
|
-
//
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
const existingOp = existingOps[op.name];
|
|
14249
|
-
if (existingOp) {
|
|
14250
|
-
// Update existing operation
|
|
14251
|
-
changes.push({ type: "operation", action: "update", key: `${dbType}/${op.name}` });
|
|
14252
|
-
if (!options.dryRun) {
|
|
14253
|
-
// #2731 B4 — the LIVE row's timestamp when the reconcile read
|
|
14254
|
-
// it, the manifest's otherwise (degraded path).
|
|
14255
|
-
const expectedOpModifiedAt = options.force
|
|
14256
|
-
? undefined
|
|
14257
|
-
: liveOpsByName?.get(op.name)?.modifiedAt ??
|
|
14258
|
-
existingOp.modifiedAt;
|
|
14259
|
-
try {
|
|
14260
|
-
const updated = await client.updateDatabaseTypeOperation(resolvedAppId, dbType, op.name, {
|
|
14261
|
-
// Include `type` so the server can detect/apply
|
|
14262
|
-
// type-in-place changes (issue #692). When the type
|
|
14263
|
-
// matches the stored value, this is a no-op.
|
|
14264
|
-
type: op.type,
|
|
14265
|
-
modelName: op.modelName,
|
|
14266
|
-
access: op.access,
|
|
14267
|
-
definition: op.definition,
|
|
14268
|
-
params: op.params,
|
|
14269
|
-
}, expectedOpModifiedAt);
|
|
14270
|
-
info(` Updated operation: ${dbType}/${op.name}`);
|
|
14271
|
-
// Recorded even when the manifest had never seen this row:
|
|
14272
|
-
// the update targeted a row live state named, and a state
|
|
14273
|
-
// that stays silent about it would send this same push
|
|
14274
|
-
// again on the next run.
|
|
14275
|
-
const opsEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
14276
|
-
if (opsEntry && updated?.modifiedAt) {
|
|
14277
|
-
if (!opsEntry.operations)
|
|
14278
|
-
opsEntry.operations = {};
|
|
14279
|
-
opsEntry.operations[op.name] = { modifiedAt: updated.modifiedAt };
|
|
14280
|
-
}
|
|
14281
|
-
}
|
|
14282
|
-
catch (err) {
|
|
14283
|
-
if (err instanceof ConflictError) {
|
|
14284
|
-
// #2731 B5 — an operation is part of this file's compared
|
|
14285
|
-
// surface, so its 409 is reported like the type's own:
|
|
14286
|
-
// against the record that is there NOW, with the field
|
|
14287
|
-
// diff (`operations` as a unit) rather than two
|
|
14288
|
-
// timestamps the operator cannot act on.
|
|
14289
|
-
conflicts.push({
|
|
14290
|
-
...(await lateConflict("operation", `${dbType}/${op.name}`, err, expectedOpModifiedAt, refetchLiveDatabaseType, databaseTypeSpec, tomlData)),
|
|
14291
|
-
});
|
|
14292
|
-
// The op was rejected, so this file did not fully land.
|
|
14293
|
-
fileHadConflict = true;
|
|
14294
|
-
}
|
|
14295
|
-
else if (err instanceof SchemaRequiredError) {
|
|
14296
|
-
schemaErrors.schemaRequired.push({
|
|
14297
|
-
type: "operation",
|
|
14298
|
-
key: `${dbType}/${op.name}`,
|
|
14299
|
-
message: err.message,
|
|
14300
|
-
});
|
|
14301
|
-
// Issue #915 (defect b): a gate-rejected op means this
|
|
14302
|
-
// file's state did NOT fully land — don't poison the
|
|
14303
|
-
// content hash (see the contentHash write below).
|
|
14304
|
-
fileHadGateRejectedOp = true;
|
|
14305
|
-
}
|
|
14306
|
-
else if (err instanceof OperationRefError) {
|
|
14307
|
-
schemaErrors.opRefs.push({
|
|
14308
|
-
type: "operation",
|
|
14309
|
-
key: `${dbType}/${op.name}`,
|
|
14310
|
-
refs: err.refs,
|
|
14311
|
-
message: err.message,
|
|
14312
|
-
});
|
|
14313
|
-
fileHadGateRejectedOp = true;
|
|
14314
|
-
}
|
|
14315
|
-
else {
|
|
14316
|
-
throw wrapEntityError(err, "update", "operation", `${dbType}/${op.name}`);
|
|
14317
|
-
}
|
|
14318
|
-
}
|
|
14319
|
-
}
|
|
14320
|
-
}
|
|
14321
|
-
else {
|
|
14322
|
-
// Create new operation
|
|
14323
|
-
changes.push({ type: "operation", action: "create", key: `${dbType}/${op.name}` });
|
|
14324
|
-
if (!options.dryRun) {
|
|
14325
|
-
try {
|
|
14326
|
-
const created = await client.createDatabaseTypeOperation(resolvedAppId, dbType, {
|
|
14327
|
-
name: op.name,
|
|
14328
|
-
type: op.type,
|
|
14329
|
-
modelName: op.modelName,
|
|
14330
|
-
access: op.access,
|
|
14331
|
-
definition: op.definition,
|
|
14332
|
-
params: op.params,
|
|
14333
|
-
});
|
|
14334
|
-
info(` Created operation: ${dbType}/${op.name}`);
|
|
14335
|
-
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
14336
|
-
if (!syncState.entities.databaseTypes[dbType].operations) {
|
|
14337
|
-
syncState.entities.databaseTypes[dbType].operations = {};
|
|
14338
|
-
}
|
|
14339
|
-
syncState.entities.databaseTypes[dbType].operations[op.name] = {
|
|
14340
|
-
modifiedAt: created?.modifiedAt || new Date().toISOString(),
|
|
14341
|
-
};
|
|
14342
|
-
}
|
|
14343
|
-
}
|
|
14344
|
-
catch (err) {
|
|
14345
|
-
if (err instanceof SchemaRequiredError) {
|
|
14346
|
-
schemaErrors.schemaRequired.push({
|
|
14347
|
-
type: "operation",
|
|
14348
|
-
key: `${dbType}/${op.name}`,
|
|
14349
|
-
message: err.message,
|
|
14350
|
-
});
|
|
14351
|
-
// Issue #915 (defect b): a gate-rejected op means this
|
|
14352
|
-
// file's state did NOT fully land — don't poison the
|
|
14353
|
-
// content hash (see the contentHash write below).
|
|
14354
|
-
fileHadGateRejectedOp = true;
|
|
14355
|
-
}
|
|
14356
|
-
else if (err instanceof OperationRefError) {
|
|
14357
|
-
schemaErrors.opRefs.push({
|
|
14358
|
-
type: "operation",
|
|
14359
|
-
key: `${dbType}/${op.name}`,
|
|
14360
|
-
refs: err.refs,
|
|
14361
|
-
message: err.message,
|
|
14362
|
-
});
|
|
14363
|
-
fileHadGateRejectedOp = true;
|
|
14364
|
-
}
|
|
14365
|
-
else {
|
|
14366
|
-
throw wrapEntityError(err, "create", "operation", `${dbType}/${op.name}`);
|
|
14367
|
-
}
|
|
14368
|
-
}
|
|
14369
|
-
}
|
|
14370
|
-
}
|
|
14371
|
-
}
|
|
14372
|
-
// Delete operations that were removed from TOML
|
|
14373
|
-
for (const existingOpName of Object.keys(existingOps)) {
|
|
14374
|
-
if (!tomlOpNames.has(existingOpName)) {
|
|
14375
|
-
changes.push({ type: "operation", action: "delete", key: `${dbType}/${existingOpName}` });
|
|
14376
|
-
if (!options.dryRun) {
|
|
14377
|
-
try {
|
|
14378
|
-
await client.deleteDatabaseTypeOperation(resolvedAppId, dbType, existingOpName);
|
|
14379
|
-
info(` Deleted operation: ${dbType}/${existingOpName}`);
|
|
14380
|
-
if (syncState?.entities?.databaseTypes?.[dbType]?.operations) {
|
|
14381
|
-
delete syncState.entities.databaseTypes[dbType].operations[existingOpName];
|
|
14382
|
-
}
|
|
14383
|
-
}
|
|
14384
|
-
catch (err) {
|
|
14385
|
-
throw wrapEntityError(err, "delete", "operation", `${dbType}/${existingOpName}`);
|
|
14386
|
-
}
|
|
14387
|
-
}
|
|
14388
|
-
}
|
|
14389
|
-
}
|
|
14390
|
-
// Process subscriptions for this type (issue #803). Mirrors the
|
|
14391
|
-
// operations reconcile above, keyed on `subscriptionKey`:
|
|
14392
|
-
// create new keys, PUT changed/existing keys, delete keys present
|
|
14393
|
-
// in SyncState but absent from the TOML. Server errors (409 key
|
|
14394
|
-
// collision, `#`-in-key 400, max-20 400, missing-required 400)
|
|
14395
|
-
// are surfaced cleanly via `wrapEntityError` — named, not a crash.
|
|
14396
|
-
const existingSubs = liveChildBaseline(liveSubsByName, syncState?.entities?.databaseTypes?.[dbType]?.subscriptions) ??
|
|
14397
|
-
((typeExistsOnServer
|
|
14398
|
-
? syncState?.entities?.databaseTypes?.[dbType]?.subscriptions
|
|
14399
|
-
: undefined) || {});
|
|
14400
|
-
const tomlSubKeys = new Set(subscriptions.map((sub) => sub.subscriptionKey));
|
|
14401
|
-
for (const sub of subscriptions) {
|
|
14402
|
-
const subKey = sub.subscriptionKey;
|
|
14403
|
-
const existingSub = existingSubs[subKey];
|
|
14404
|
-
if (existingSub) {
|
|
14405
|
-
// Update existing subscription (PUT — idempotent).
|
|
14406
|
-
changes.push({ type: "subscription", action: "update", key: `${dbType}/${subKey}` });
|
|
14407
|
-
if (!options.dryRun) {
|
|
14408
|
-
// #2731 B4 — the LIVE row's timestamp when the reconcile read
|
|
14409
|
-
// it, the manifest's otherwise (degraded path). Subscriptions
|
|
14410
|
-
// are part of a converted type's push units, so a concurrent
|
|
14411
|
-
// server edit has to be a 409 here too, not a silent
|
|
14412
|
-
// overwrite.
|
|
14413
|
-
const expectedSubModifiedAt = options.force
|
|
14414
|
-
? undefined
|
|
14415
|
-
: liveSubsByName?.get(subKey)?.modifiedAt ??
|
|
14416
|
-
existingSub.modifiedAt;
|
|
14417
|
-
try {
|
|
14418
|
-
const updated = await client.updateDatabaseTypeSubscription(resolvedAppId, dbType, subKey, {
|
|
14419
|
-
displayName: sub.displayName,
|
|
14420
|
-
modelName: sub.modelName,
|
|
14421
|
-
filter: sub.filter,
|
|
14422
|
-
access: sub.access,
|
|
14423
|
-
description: sub.description,
|
|
14424
|
-
select: sub.select,
|
|
14425
|
-
emit: sub.emit,
|
|
14426
|
-
params: sub.params,
|
|
14427
|
-
status: sub.status,
|
|
14428
|
-
}, expectedSubModifiedAt);
|
|
14429
|
-
info(` Updated subscription: ${dbType}/${subKey}`);
|
|
14430
|
-
// Gate the SyncState write on a server-returned
|
|
14431
|
-
// `modifiedAt`, mirroring the operations precedent above
|
|
14432
|
-
// (don't fall back to a fresh local timestamp) — and record
|
|
14433
|
-
// a row the manifest had never seen, for the same reason.
|
|
14434
|
-
const subsEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
14435
|
-
if (subsEntry && updated?.modifiedAt) {
|
|
14436
|
-
if (!subsEntry.subscriptions)
|
|
14437
|
-
subsEntry.subscriptions = {};
|
|
14438
|
-
subsEntry.subscriptions[subKey] = { modifiedAt: updated.modifiedAt };
|
|
14439
|
-
}
|
|
14440
|
-
}
|
|
14441
|
-
catch (err) {
|
|
14442
|
-
if (err instanceof ConflictError) {
|
|
14443
|
-
conflicts.push({
|
|
14444
|
-
...(await lateConflict("subscription", `${dbType}/${subKey}`, err, expectedSubModifiedAt, refetchLiveDatabaseType, databaseTypeSpec, tomlData)),
|
|
14445
|
-
});
|
|
14446
|
-
// The subscription was rejected, so this file did not
|
|
14447
|
-
// fully land — the baseline stamp below must not claim it
|
|
14448
|
-
// did.
|
|
14449
|
-
fileHadConflict = true;
|
|
14450
|
-
}
|
|
14451
|
-
else {
|
|
14452
|
-
throw wrapEntityError(err, "update", "subscription", `${dbType}/${subKey}`);
|
|
14453
|
-
}
|
|
14454
|
-
}
|
|
14455
|
-
}
|
|
14456
|
-
}
|
|
14457
|
-
else {
|
|
14458
|
-
// Create new subscription (POST).
|
|
14459
|
-
changes.push({ type: "subscription", action: "create", key: `${dbType}/${subKey}` });
|
|
14460
|
-
if (!options.dryRun) {
|
|
14461
|
-
try {
|
|
14462
|
-
const created = await client.createDatabaseTypeSubscription(resolvedAppId, dbType, {
|
|
14463
|
-
subscriptionKey: subKey,
|
|
14464
|
-
displayName: sub.displayName,
|
|
14465
|
-
modelName: sub.modelName,
|
|
14466
|
-
filter: sub.filter,
|
|
14467
|
-
access: sub.access,
|
|
14468
|
-
description: sub.description,
|
|
14469
|
-
select: sub.select,
|
|
14470
|
-
emit: sub.emit,
|
|
14471
|
-
params: sub.params,
|
|
14472
|
-
status: sub.status,
|
|
14473
|
-
});
|
|
14474
|
-
info(` Created subscription: ${dbType}/${subKey}`);
|
|
14475
|
-
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
14476
|
-
if (!syncState.entities.databaseTypes[dbType].subscriptions) {
|
|
14477
|
-
syncState.entities.databaseTypes[dbType].subscriptions = {};
|
|
14478
|
-
}
|
|
14479
|
-
syncState.entities.databaseTypes[dbType].subscriptions[subKey] = {
|
|
14480
|
-
modifiedAt: created?.modifiedAt || new Date().toISOString(),
|
|
14481
|
-
};
|
|
14482
|
-
}
|
|
14483
|
-
}
|
|
14484
|
-
catch (err) {
|
|
14485
|
-
throw wrapEntityError(err, "create", "subscription", `${dbType}/${subKey}`);
|
|
14486
|
-
}
|
|
14487
|
-
}
|
|
14488
|
-
}
|
|
14489
|
-
}
|
|
14490
|
-
// Delete subscriptions that were removed from TOML (hard-delete on
|
|
14491
|
-
// current `main`, PR #787 — frees the key for reuse). A 409 on
|
|
14492
|
-
// recreate is impossible here since we delete first; the pull-side
|
|
14493
|
-
// "active only" filter (controller `list` excludes archived)
|
|
14494
|
-
// agrees because hard-delete leaves no archived row behind.
|
|
14495
|
-
for (const existingSubKey of Object.keys(existingSubs)) {
|
|
14496
|
-
if (!tomlSubKeys.has(existingSubKey)) {
|
|
14497
|
-
changes.push({ type: "subscription", action: "delete", key: `${dbType}/${existingSubKey}` });
|
|
14498
|
-
if (!options.dryRun) {
|
|
14499
|
-
try {
|
|
14500
|
-
await client.deleteDatabaseTypeSubscription(resolvedAppId, dbType, existingSubKey);
|
|
14501
|
-
info(` Deleted subscription: ${dbType}/${existingSubKey}`);
|
|
14502
|
-
if (syncState?.entities?.databaseTypes?.[dbType]?.subscriptions) {
|
|
14503
|
-
delete syncState.entities.databaseTypes[dbType].subscriptions[existingSubKey];
|
|
14504
|
-
}
|
|
14505
|
-
}
|
|
14506
|
-
catch (err) {
|
|
14507
|
-
throw wrapEntityError(err, "delete", "subscription", `${dbType}/${existingSubKey}`);
|
|
14508
|
-
}
|
|
14509
|
-
}
|
|
14510
|
-
}
|
|
14511
|
-
}
|
|
14512
|
-
// Refresh the file's content hash after processing operations +
|
|
14513
|
-
// subscriptions (issue #803, behavior g). The type-config update
|
|
14514
|
-
// block only refreshes the hash when a type-level field changed
|
|
14515
|
-
// (its `updateData` was non-empty); an operations- or
|
|
14516
|
-
// subscriptions-only edit left a stale hash, so the next push
|
|
14517
|
-
// would re-PUT every op/sub instead of skipping the unchanged
|
|
14518
|
-
// file. Recompute here so a truly-unchanged subsequent push skips
|
|
14519
|
-
// the whole file via `shouldPushFile`. `--force` bypasses that
|
|
14520
|
-
// skip, so forced re-pushes are unaffected.
|
|
14521
|
-
//
|
|
14522
|
-
// Issue #915 (defect b): when the gate rejected an op for this
|
|
14523
|
-
// file, do NOT record a matching content hash. The db-type itself
|
|
14524
|
-
// may have been created (the push is non-transactional), but the
|
|
14525
|
-
// blocked op never landed — a matching hash would make
|
|
14526
|
-
// `shouldPushFile` short-circuit the next push and hide the missing
|
|
14527
|
-
// op as "in sync". Clear the hash instead of writing it: an empty
|
|
14528
|
-
// hash forces `shouldPushFile` to re-evaluate the file, so a
|
|
14529
|
-
// subsequent `config push --dry-run` still surfaces the op as drift
|
|
14530
|
-
// and a corrected re-push converges without manual file edits. We
|
|
14531
|
-
// clear (rather than merely skip) so the fresh-type create path's
|
|
14532
|
-
// earlier optimistic hash write (see `createDatabaseTypeConfig`
|
|
14533
|
-
// above) is also invalidated.
|
|
14534
|
-
//
|
|
14535
|
-
// #2731 B4: the semantic baseline is refreshed here too, and
|
|
14536
|
-
// cleared alongside the byte hash when an op was gate-rejected —
|
|
14537
|
-
// a baseline claiming convergence would make the next run read the
|
|
14538
|
-
// missing op as server drift rather than as work still to do. A
|
|
14539
|
-
// 409 on the type or one of its ops means the same thing: the
|
|
14540
|
-
// server refused the write, so this file did not land either.
|
|
13654
|
+
// Refresh the file's content hash and semantic baseline (#2731 B4)
|
|
13655
|
+
// after the type's writes. The type-config update block only
|
|
13656
|
+
// refreshes the hash when a type-level field changed; recomputing
|
|
13657
|
+
// here lets a truly-unchanged next push skip the file. A 409 means
|
|
13658
|
+
// the server refused the write, so this file did not land: clear
|
|
13659
|
+
// both rather than claim convergence.
|
|
14541
13660
|
if (!options.dryRun && syncState?.entities?.databaseTypes?.[dbType]) {
|
|
14542
|
-
if (
|
|
13661
|
+
if (fileHadConflict) {
|
|
14543
13662
|
syncState.entities.databaseTypes[dbType].contentHash = "";
|
|
14544
13663
|
syncState.entities.databaseTypes[dbType].semanticHash = undefined;
|
|
14545
13664
|
}
|
|
14546
13665
|
else {
|
|
14547
13666
|
syncState.entities.databaseTypes[dbType].contentHash =
|
|
14548
13667
|
computeFileHash(filePath);
|
|
14549
|
-
// B4 — the baseline reads back what the server now holds
|
|
14550
|
-
//
|
|
13668
|
+
// B4 — the baseline reads back what the server now holds.
|
|
13669
|
+
// Taking it from the local file would
|
|
14551
13670
|
// stamp a value the handlers normalized as the state both sides
|
|
14552
13671
|
// agreed on, and the next local edit would report a conflict
|
|
14553
13672
|
// against a difference this push itself created. A read that
|
|
@@ -14579,6 +13698,9 @@ What a push guarantees (two stages):
|
|
|
14579
13698
|
// import that escapes the config tree aborts this function before
|
|
14580
13699
|
// anything is sent for it (behavior 12).
|
|
14581
13700
|
const functionsPushDir = join(configDir, "functions");
|
|
13701
|
+
// #4437 — functions this push failed to apply that the server does not
|
|
13702
|
+
// hold either, so the deferred agent leg can name them as the cause.
|
|
13703
|
+
const unappliedFunctionKeys = new Set();
|
|
14582
13704
|
if (existsSync(functionsPushDir)) {
|
|
14583
13705
|
// #3182 — the typed handle. Written BEFORE the bundles are built, so
|
|
14584
13706
|
// an author who runs `config push` after editing a schema gets the
|
|
@@ -14673,6 +13795,15 @@ What a push guarantees (two stages):
|
|
|
14673
13795
|
withDerivedIdentity(tomlData, "function", { key });
|
|
14674
13796
|
const existingEntry = syncState?.entities?.functions?.[key];
|
|
14675
13797
|
const liveFunction = liveFunctions.get(key);
|
|
13798
|
+
// An archived row is a tombstone: the server refuses an agent
|
|
13799
|
+
// naming it exactly as it refuses one naming no function at all.
|
|
13800
|
+
const serverHoldsFunction = liveFunction
|
|
13801
|
+
? String(liveFunction.status ?? "active") !== "archived"
|
|
13802
|
+
: !functionsLiveOk && !!existingEntry?.id;
|
|
13803
|
+
const markUnapplied = () => {
|
|
13804
|
+
if (!serverHoldsFunction)
|
|
13805
|
+
unappliedFunctionKeys.add(key);
|
|
13806
|
+
};
|
|
14676
13807
|
// Build first: nothing is sent for a function whose code does not
|
|
14677
13808
|
// compile or whose imports leave the tree.
|
|
14678
13809
|
let push;
|
|
@@ -14692,6 +13823,7 @@ What a push guarantees (two stages):
|
|
|
14692
13823
|
key,
|
|
14693
13824
|
message: err.message,
|
|
14694
13825
|
});
|
|
13826
|
+
markUnapplied();
|
|
14695
13827
|
continue;
|
|
14696
13828
|
}
|
|
14697
13829
|
// #3455 (D3455-004) — the project declares a document schema this
|
|
@@ -14718,6 +13850,7 @@ What a push guarantees (two stages):
|
|
|
14718
13850
|
key,
|
|
14719
13851
|
message: failure.message,
|
|
14720
13852
|
});
|
|
13853
|
+
markUnapplied();
|
|
14721
13854
|
continue;
|
|
14722
13855
|
}
|
|
14723
13856
|
}
|
|
@@ -14835,6 +13968,7 @@ What a push guarantees (two stages):
|
|
|
14835
13968
|
// the same contract `applyFailures` gives every other type; a throw
|
|
14836
13969
|
// here would let one refusal block a whole repository.
|
|
14837
13970
|
let functionId = liveFunction?.functionId ?? existingEntry?.id;
|
|
13971
|
+
let createdThisPush = false;
|
|
14838
13972
|
let pushedVersion;
|
|
14839
13973
|
try {
|
|
14840
13974
|
if (!functionId) {
|
|
@@ -14859,6 +13993,7 @@ What a push guarantees (two stages):
|
|
|
14859
13993
|
return existing;
|
|
14860
13994
|
});
|
|
14861
13995
|
functionId = created.functionId;
|
|
13996
|
+
createdThisPush = true;
|
|
14862
13997
|
info(` Created function: ${key}`);
|
|
14863
13998
|
}
|
|
14864
13999
|
else if (headerAction !== "skip") {
|
|
@@ -14881,6 +14016,11 @@ What a push guarantees (two stages):
|
|
|
14881
14016
|
key,
|
|
14882
14017
|
message: String(err?.message ?? err),
|
|
14883
14018
|
});
|
|
14019
|
+
// A refused create, or a refused push to a row the server does
|
|
14020
|
+
// not hold live (an archived tombstone). A row this push created
|
|
14021
|
+
// is held even when its version was refused.
|
|
14022
|
+
if (!createdThisPush)
|
|
14023
|
+
markUnapplied();
|
|
14884
14024
|
continue;
|
|
14885
14025
|
}
|
|
14886
14026
|
// #3455 (D3455-010) — did the server keep the document schema it
|
|
@@ -14943,8 +14083,15 @@ What a push guarantees (two stages):
|
|
|
14943
14083
|
}
|
|
14944
14084
|
// #3798 — the agent prompts, now that every function they name that
|
|
14945
14085
|
// this push applies is on the server (see `pushPromptFiles`).
|
|
14086
|
+
// #4437 — except an agent naming a function that failed above and that
|
|
14087
|
+
// the server does not hold: the server would refuse it as naming a
|
|
14088
|
+
// function the app does not have, hiding the function's own failure.
|
|
14946
14089
|
if (deferredAgentPromptFiles.length > 0) {
|
|
14947
|
-
|
|
14090
|
+
const { apply, held } = holdAgentPromptFilesForUnappliedFunctions(promptsDir, deferredAgentPromptFiles, unappliedFunctionKeys);
|
|
14091
|
+
for (const { key, message } of held) {
|
|
14092
|
+
applyFailures.push({ type: "prompt", key, message });
|
|
14093
|
+
}
|
|
14094
|
+
await pushPromptFiles(apply);
|
|
14948
14095
|
}
|
|
14949
14096
|
// Process group type configs
|
|
14950
14097
|
const groupTypeConfigsDir = join(configDir, "group-type-configs");
|
|
@@ -17285,12 +16432,9 @@ What a push guarantees (two stages):
|
|
|
17285
16432
|
// Blob buckets — #2880: the list row is the record. #4116: a bucket
|
|
17286
16433
|
// names its rule set, so its rows take the type configs' gate.
|
|
17287
16434
|
compareByDefinition("blob-bucket", "blobBuckets", localBlobBucketsParsed, new Map((blobBucketsDiffResult.items || []).map((b) => [b.bucketKey, b])), blobBucketsOutcomeDiff.ok && ruleSetsOk);
|
|
17288
|
-
// database-type-configs: the
|
|
17289
|
-
//
|
|
17290
|
-
//
|
|
17291
|
-
// "content not compared" rather than aborting the whole diff (#1485
|
|
17292
|
-
// edge case). Both operations AND subscriptions are included so an
|
|
17293
|
-
// op-only or subscription-only change is not falsely reported as Synced.
|
|
16435
|
+
// database-type-configs: the listing carries the whole type, so each
|
|
16436
|
+
// row compares on its own. (#3189 — the operations and subscriptions
|
|
16437
|
+
// that used to be fetched per type beside it are retired.)
|
|
17294
16438
|
for (const [key, parsed] of localDatabaseTypesParsed) {
|
|
17295
16439
|
const remote = remoteDatabaseTypes.get(key);
|
|
17296
16440
|
if (!remote) {
|
|
@@ -17303,23 +16447,10 @@ What a push guarantees (two stages):
|
|
|
17303
16447
|
hint = "content not compared (rule sets unavailable)";
|
|
17304
16448
|
}
|
|
17305
16449
|
else {
|
|
17306
|
-
|
|
17307
|
-
|
|
17308
|
-
|
|
17309
|
-
|
|
17310
|
-
if (!opsOutcome.ok || !subsOutcome.ok) {
|
|
17311
|
-
hint = "content not compared (fetch failed)";
|
|
17312
|
-
}
|
|
17313
|
-
else {
|
|
17314
|
-
const operations = Array.isArray(opsOutcome.value) ? opsOutcome.value : [];
|
|
17315
|
-
const subscriptions = Array.isArray(subsOutcome.value) ? subsOutcome.value : [];
|
|
17316
|
-
// The shared verdict (#3267): a Modified row names the
|
|
17317
|
-
// operation, subscription, model field or `[type]` scalar it
|
|
17318
|
-
// differs in, and a comparison that throws degrades this one
|
|
17319
|
-
// row to "not compared". The ops/subscriptions are the remote
|
|
17320
|
-
// side's extra only — the file already carries its own.
|
|
17321
|
-
({ status, hint } = compareLocalToRemote(configDiffSpec("database-type-config"), parsed, remote, diffMaps, { operations, subscriptions }));
|
|
17322
|
-
}
|
|
16450
|
+
// The shared verdict (#3267): a Modified row names the model field
|
|
16451
|
+
// or `[type]` scalar it differs in, and a comparison that throws
|
|
16452
|
+
// degrades this one row to "not compared".
|
|
16453
|
+
({ status, hint } = compareLocalToRemote(configDiffSpec("database-type-config"), parsed, remote, diffMaps, {}));
|
|
17323
16454
|
}
|
|
17324
16455
|
differences.push({ type: "database-type-config", key, status, hint });
|
|
17325
16456
|
}
|
|
@@ -17767,8 +16898,13 @@ What a push guarantees (two stages):
|
|
|
17767
16898
|
// next, so an in-loop abort would leave earlier files rewritten. Every
|
|
17768
16899
|
// offending file is reported, so the author fixes the tree once.
|
|
17769
16900
|
const retiredKeyErrors = [];
|
|
16901
|
+
const databaseTypeSurface = CONFIG_SURFACES.find((surface) => surface.label === "database-type-config");
|
|
17770
16902
|
for (const file of dbFiles) {
|
|
17771
|
-
const
|
|
16903
|
+
const tomlData = parseTomlFile(join(dbTypesDir, file));
|
|
16904
|
+
// #3189 — a retired TABLE too (`[[operations]]`, `[[subscriptions]]`):
|
|
16905
|
+
// the rewrite below would otherwise drop it without a word.
|
|
16906
|
+
retiredKeyErrors.push(...configDocumentShapeErrors(`database-type-configs/${file}`, tomlData, databaseTypeSurface));
|
|
16907
|
+
const message = configUnknownKeyError(`database-type-configs/${file}`, tomlData, DATABASE_TYPE_TABLE);
|
|
17772
16908
|
if (message)
|
|
17773
16909
|
retiredKeyErrors.push(message);
|
|
17774
16910
|
}
|
|
@@ -17786,30 +16922,9 @@ What a push guarantees (two stages):
|
|
|
17786
16922
|
const filePath = join(dbTypesDir, file);
|
|
17787
16923
|
const rawBefore = readFileSync(filePath, "utf-8");
|
|
17788
16924
|
const tomlData = parseTomlFile(filePath);
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
let typeConfig;
|
|
17792
|
-
let operations;
|
|
17793
|
-
let subscriptions;
|
|
17794
|
-
try {
|
|
17795
|
-
({ typeConfig, operations, subscriptions } =
|
|
17796
|
-
parseDatabaseTypeToml(tomlData));
|
|
17797
|
-
}
|
|
17798
|
-
catch (err) {
|
|
17799
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
17800
|
-
error(` ${file}: ${err.message}`);
|
|
17801
|
-
error(`Aborting migrate-toml: subscription \`access\`/\`accessRule\` conflict in ${file}.`);
|
|
17802
|
-
process.exit(1);
|
|
17803
|
-
}
|
|
17804
|
-
throw err;
|
|
17805
|
-
}
|
|
17806
|
-
// Force-native: ignore existing hints so every TOMLable field
|
|
17807
|
-
// ends up in nested-table form. Subscriptions are preserved on the
|
|
17808
|
-
// rewrite (issue #803) so migrate-toml never drops them.
|
|
17809
|
-
const rewritten = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
17810
|
-
defaultForm: "native",
|
|
16925
|
+
const { typeConfig } = parseDatabaseTypeToml(tomlData);
|
|
16926
|
+
const rewritten = serializeDatabaseType(typeConfig, ruleSetIdToName, {
|
|
17811
16927
|
logger: (msg) => info(` ${msg}`),
|
|
17812
|
-
subscriptions,
|
|
17813
16928
|
});
|
|
17814
16929
|
if (rewritten === rawBefore) {
|
|
17815
16930
|
unchangedCount++;
|