primitive-admin 1.2.0-alpha.3 → 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/agent-sessions.js +4 -0
- package/dist/src/commands/agent-sessions.js.map +1 -1
- 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 +78 -403
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +32 -2
- package/dist/src/commands/documents.js +344 -711
- 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/prompts.d.ts +7 -0
- package/dist/src/commands/prompts.js +44 -0
- package/dist/src/commands/prompts.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 +43 -110
- package/dist/src/commands/sync.js +356 -1251
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/aggregate-output.d.ts +25 -0
- package/dist/src/lib/aggregate-output.js +53 -0
- package/dist/src/lib/aggregate-output.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +48 -204
- package/dist/src/lib/api-client.js +57 -321
- 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 +15 -10
- 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/dbGenerator.js +5 -16
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +5 -13
- package/dist/src/lib/db-codegen/dbNaming.js +5 -17
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +7 -56
- package/dist/src/lib/db-codegen/dbTemplates.js +25 -125
- package/dist/src/lib/db-codegen/dbTemplates.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 +60 -2
- 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-sync.d.ts +3 -16
- package/dist/src/lib/function-sync.js +14 -18
- package/dist/src/lib/function-sync.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 +212 -49
- package/dist/src/lib/generated-config-surfaces.js +759 -184
- 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/prompt-schema-codegen.js +5 -2
- package/dist/src/lib/prompt-schema-codegen.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/dbGenerator.d.ts +4 -3
- package/dist/src/lib/swift-codegen/dbGenerator.js +14 -117
- package/dist/src/lib/swift-codegen/dbGenerator.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.d.ts +5 -5
- package/dist/src/lib/swift-codegen/generator.js +35 -40
- 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-codegen/generator.d.ts +7 -7
- package/dist/src/lib/workflow-codegen/generator.js +16 -23
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +6 -12
- package/dist/src/lib/workflow-codegen/invokerIR.js +4 -5
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +2 -0
- package/dist/src/lib/workflow-toml-validator.js +28 -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,14 +34,17 @@ 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
|
|
42
41
|
// preflight refuses exactly what the admin routes refuse.
|
|
43
42
|
import { promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
|
|
44
43
|
// #3798 — the `[prompt.agent]` declaration rule, vendored the same way.
|
|
45
|
-
import { agentDeclarationFunctionKeys,
|
|
44
|
+
import { agentDeclarationFunctionKeys, resolveAgentDeclaration, } from "../lib/generated-config-surfaces.js";
|
|
45
|
+
// #4262 — the agent test case's shape rule (`[test.agent]`), vendored the same
|
|
46
|
+
// way, so the preflight refuses exactly the sidecars the admin routes refuse.
|
|
47
|
+
import { AGENT_TEST_CASE_FIELDS, resolveAgentTestCase, } from "../lib/generated-config-surfaces.js";
|
|
46
48
|
// #3799 — the meaning-preserving schema translation, vendored the same way, so
|
|
47
49
|
// the preflight refuses exactly the schemas the admin routes refuse.
|
|
48
50
|
import { agentSchemaTranslationRefusals } from "../lib/generated-config-surfaces.js";
|
|
@@ -51,7 +53,7 @@ import { agentSchemaTranslationRefusals } from "../lib/generated-config-surfaces
|
|
|
51
53
|
import { resolvePromptStrictOutput } from "../lib/generated-config-surfaces.js";
|
|
52
54
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
53
55
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
54
|
-
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity,
|
|
56
|
+
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
55
57
|
import { newerVersionLine, pullNewerVersionLine, rollbackDiffHint, } from "../lib/function-versions.js";
|
|
56
58
|
import { FunctionBuildError } from "../lib/function-bundle.js";
|
|
57
59
|
// #3182 — the local half of capability validation. The grammar is the vendored
|
|
@@ -1672,9 +1674,9 @@ export function serializeBlobBucket(bucket, ruleSetIdToName = new Map(), logger)
|
|
|
1672
1674
|
* `config push` builds its bodies from and `config diff` compares.
|
|
1673
1675
|
*
|
|
1674
1676
|
* Read in CREATE mode, so the immutable `ttlTier` stays nameable. The rule set
|
|
1675
|
-
* is left as `_ruleSetName` for `resolveRuleSetReference
|
|
1676
|
-
* `ruleSetId`
|
|
1677
|
-
*
|
|
1677
|
+
* is left as `_ruleSetName` for `resolveRuleSetReference`. The id spelling
|
|
1678
|
+
* `ruleSetId` is a retired key (#4283): the preflight refuses a file carrying
|
|
1679
|
+
* it, and this parse never reads it.
|
|
1678
1680
|
*
|
|
1679
1681
|
* An omitted `name` stays omitted: the update leaves the server's name alone,
|
|
1680
1682
|
* and only the create body falls back to the key.
|
|
@@ -1697,43 +1699,8 @@ export function parseBlobBucketToml(doc) {
|
|
|
1697
1699
|
// Trimmed, as `applyRuleSetReference` does for the type configs.
|
|
1698
1700
|
entity._ruleSetName = section.ruleSetName.trim();
|
|
1699
1701
|
}
|
|
1700
|
-
else if (typeof section.ruleSetId === "string" && section.ruleSetId !== "") {
|
|
1701
|
-
entity.ruleSetId = section.ruleSetId;
|
|
1702
|
-
}
|
|
1703
1702
|
return entity;
|
|
1704
1703
|
}
|
|
1705
|
-
/**
|
|
1706
|
-
* The advisory `config push` prints for a bucket file still naming its rule set
|
|
1707
|
-
* by id (#4116). The key is deprecated, not retired: the file pushes.
|
|
1708
|
-
*/
|
|
1709
|
-
export function blobBucketDeprecatedKeyWarning(filePath, tomlData) {
|
|
1710
|
-
const bucket = tomlData?.bucket;
|
|
1711
|
-
if (!bucket || bucket.ruleSetId === undefined)
|
|
1712
|
-
return null;
|
|
1713
|
-
// Beside `ruleSetName` it is a refusal instead (`blobBucketFileErrors`).
|
|
1714
|
-
if (bucket.ruleSetName !== undefined)
|
|
1715
|
-
return null;
|
|
1716
|
-
return (`${filePath}: bucket.ruleSetId is deprecated — a rule-set id belongs to one app, ` +
|
|
1717
|
-
`so a copied tree cannot resolve it; write ruleSetName = "<name>" ` +
|
|
1718
|
-
"(config pull rewrites the file). Retiring the key is a later change.");
|
|
1719
|
-
}
|
|
1720
|
-
/** A bucket file's own refusals, decided from the file alone (#4116). */
|
|
1721
|
-
export function blobBucketFileErrors(filePath, tomlData) {
|
|
1722
|
-
const bucket = tomlData?.bucket;
|
|
1723
|
-
if (bucket && bucket.ruleSetId !== undefined && bucket.ruleSetName !== undefined) {
|
|
1724
|
-
return [`${filePath}: bucket.ruleSetId is set beside bucket.ruleSetName; keep ruleSetName`];
|
|
1725
|
-
}
|
|
1726
|
-
// The deprecated spelling is a TOML-only key, so the declared-type check
|
|
1727
|
-
// reads only `ruleSetName`; the old key keeps the field's type under its
|
|
1728
|
-
// own name, or a mistyped id would be dropped from the body unreported.
|
|
1729
|
-
const field = BLOB_BUCKET_TABLE.fields.find((f) => f.field === "ruleSetId");
|
|
1730
|
-
if (!field)
|
|
1731
|
-
return [];
|
|
1732
|
-
return configDeclaredTypeErrors(filePath, tomlData, {
|
|
1733
|
-
...BLOB_BUCKET_TABLE,
|
|
1734
|
-
fields: [{ ...field, tomlKey: "ruleSetId" }],
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
1704
|
/**
|
|
1738
1705
|
* Build the PATCH payload for a blob-bucket update from local TOML.
|
|
1739
1706
|
*
|
|
@@ -1947,33 +1914,33 @@ export function serializePrompt(prompt, logger) {
|
|
|
1947
1914
|
return stringifyConfigToml(data);
|
|
1948
1915
|
}
|
|
1949
1916
|
/**
|
|
1950
|
-
* The `config push` warning for a
|
|
1917
|
+
* The `config push` warning for a caller workflow pushed with no `accessRule`
|
|
1918
|
+
* (#2652).
|
|
1951
1919
|
*
|
|
1952
1920
|
* Returns `null` when a rule is declared — and for a `runAs = "system"`
|
|
1953
1921
|
* workflow, which rejects any rule and cannot be started by a member at all.
|
|
1954
|
-
* Otherwise it names the
|
|
1955
|
-
* because after the #2652 flip a ruleless
|
|
1956
|
-
*
|
|
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
|
+
*
|
|
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.
|
|
1957
1929
|
*
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
* caveat needs to see it retracted.
|
|
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.
|
|
1964
1935
|
*/
|
|
1965
1936
|
export function missingAccessRuleWarning(opts) {
|
|
1966
1937
|
if (typeof opts.accessRule === "string" && opts.accessRule.trim() !== "") {
|
|
1967
1938
|
return null;
|
|
1968
1939
|
}
|
|
1969
|
-
if (opts.
|
|
1940
|
+
if (opts.runAs === "system")
|
|
1970
1941
|
return null;
|
|
1971
1942
|
const restore = `Add accessRule = "true" to allow any app member, or a CEL rule ` +
|
|
1972
1943
|
`naming who may call it.`;
|
|
1973
|
-
if (opts.kind === "prompt") {
|
|
1974
|
-
return (`prompt '${opts.key}' has no accessRule — all non-admin executions are ` +
|
|
1975
|
-
`denied. ${restore}`);
|
|
1976
|
-
}
|
|
1977
1944
|
return (`workflow '${opts.key}' declares no accessRule — pushing this file clears ` +
|
|
1978
1945
|
`any rule stored server-side, and a workflow with no stored rule denies ` +
|
|
1979
1946
|
`all non-admin starts. ${restore}`);
|
|
@@ -2360,7 +2327,16 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2360
2327
|
*/
|
|
2361
2328
|
export function configDocumentShapeErrors(filePath, tomlData, surface) {
|
|
2362
2329
|
const errors = [];
|
|
2363
|
-
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));
|
|
2364
2340
|
if (unknown.length > 0) {
|
|
2365
2341
|
errors.push(unrecognizedPushTablesMessage(filePath, unknown));
|
|
2366
2342
|
}
|
|
@@ -2595,77 +2571,22 @@ export async function runConfigPushPreflight(input) {
|
|
|
2595
2571
|
}
|
|
2596
2572
|
}
|
|
2597
2573
|
}
|
|
2598
|
-
// Validate all database-type TOMLs
|
|
2599
|
-
// 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.)
|
|
2600
2578
|
const preflightDbTypesDir = join(configDir, "database-type-configs");
|
|
2601
2579
|
if (existsSync(preflightDbTypesDir)) {
|
|
2602
2580
|
for (const file of selectFiles("database-type-config", preflightDbTypesDir, readdirSync(preflightDbTypesDir).filter((f) => f.endsWith(".toml")))) {
|
|
2603
2581
|
const filePath = join(preflightDbTypesDir, file);
|
|
2604
|
-
let rawToml;
|
|
2605
|
-
let tomlData;
|
|
2606
2582
|
try {
|
|
2607
|
-
|
|
2608
|
-
tomlData = parseTomlFile(filePath);
|
|
2583
|
+
parseDatabaseTypeToml(parseTomlFile(filePath));
|
|
2609
2584
|
}
|
|
2610
2585
|
catch (err) {
|
|
2611
2586
|
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2612
|
-
continue;
|
|
2613
|
-
}
|
|
2614
|
-
let operations = [];
|
|
2615
|
-
try {
|
|
2616
|
-
({ operations } = parseDatabaseTypeToml(tomlData));
|
|
2617
|
-
}
|
|
2618
|
-
catch (err) {
|
|
2619
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
2620
|
-
preflightValidationErrors.push(` ${file}: ${err.message}`);
|
|
2621
|
-
continue;
|
|
2622
|
-
}
|
|
2623
|
-
preflightValidationErrors.push(` ${file}: ${err?.message || String(err)}`);
|
|
2624
|
-
continue;
|
|
2625
|
-
}
|
|
2626
|
-
const validation = validateOperations({
|
|
2627
|
-
filePath,
|
|
2628
|
-
rawToml,
|
|
2629
|
-
operations,
|
|
2630
|
-
});
|
|
2631
|
-
for (const e of validation.errors) {
|
|
2632
|
-
preflightValidationErrors.push(` ${formatIssue(e)}`);
|
|
2633
|
-
}
|
|
2634
|
-
// Issue #1422: non-fatal deprecation notice. A `celContextAccess`
|
|
2635
|
-
// / `metadataAccess` gate on a database type config is a single CEL
|
|
2636
|
-
// expression that gates BOTH read and update of the database CEL
|
|
2637
|
-
// context — granting read implicitly grants update. Steer operators
|
|
2638
|
-
// to resource metadata categories (separate readRule/writeRule).
|
|
2639
|
-
// This is advisory only: the push continues and the runtime gate is
|
|
2640
|
-
// unchanged. It fires only when the deprecated key is actually
|
|
2641
|
-
// present, so metadata-category-only configs never trip it.
|
|
2642
|
-
const typeSection = tomlData.type || {};
|
|
2643
|
-
if (typeSection.celContextAccess || typeSection.metadataAccess) {
|
|
2644
|
-
const usedKey = typeSection.celContextAccess
|
|
2645
|
-
? "celContextAccess"
|
|
2646
|
-
: "metadataAccess";
|
|
2647
|
-
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.`);
|
|
2648
2587
|
}
|
|
2649
2588
|
}
|
|
2650
2589
|
}
|
|
2651
|
-
// #4116 — the deprecated `ruleSetId` spelling in a bucket file. Advisory
|
|
2652
|
-
// only: the file pushes. Bounded by `--only`, like the notice above.
|
|
2653
|
-
const preflightBucketsDir = join(configDir, "blob-buckets");
|
|
2654
|
-
if (existsSync(preflightBucketsDir)) {
|
|
2655
|
-
for (const file of selectFiles("blob-bucket", preflightBucketsDir, readdirSync(preflightBucketsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2656
|
-
let tomlData;
|
|
2657
|
-
try {
|
|
2658
|
-
tomlData = parseTomlFile(join(preflightBucketsDir, file));
|
|
2659
|
-
}
|
|
2660
|
-
catch {
|
|
2661
|
-
// A file that does not parse is reported by the collector below.
|
|
2662
|
-
continue;
|
|
2663
|
-
}
|
|
2664
|
-
const advice = blobBucketDeprecatedKeyWarning(`blob-buckets/${file}`, tomlData);
|
|
2665
|
-
if (advice)
|
|
2666
|
-
warn(` ${advice}`);
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
2590
|
// Validate app.toml settings up-front (issue #1033). A mistyped value
|
|
2670
2591
|
// is a hard error that must abort BEFORE any mutation — same
|
|
2671
2592
|
// fail-before-mutate contract as the vars preflight below. The apply
|
|
@@ -2781,7 +2702,7 @@ export async function runConfigPushPreflight(input) {
|
|
|
2781
2702
|
preflightValidationErrors.push(` ${message}`);
|
|
2782
2703
|
}
|
|
2783
2704
|
// #3320 — and everything else the file decides on its own: the
|
|
2784
|
-
// `entry`, the
|
|
2705
|
+
// `entry`, the retired runtime keys it refuses (#3188), the trigger
|
|
2785
2706
|
// rules, and a webhook trigger's signing-secret requirement and
|
|
2786
2707
|
// reference shape. These used to be raised inside the apply loop,
|
|
2787
2708
|
// after webhooks, integrations, vars and database types had
|
|
@@ -2796,19 +2717,6 @@ export async function runConfigPushPreflight(input) {
|
|
|
2796
2717
|
})) {
|
|
2797
2718
|
preflightValidationErrors.push(` ${filePath}: ${message}`);
|
|
2798
2719
|
}
|
|
2799
|
-
// #3482 — the retired runtime keys. NOT refusals through phase 4:
|
|
2800
|
-
// the file pushes with every other key applied, and the author
|
|
2801
|
-
// hears which line to delete before phase 5 refuses it. On
|
|
2802
|
-
// stderr, like every other advisory, so a scripted push reading
|
|
2803
|
-
// stdout is unaffected. Inside this loop rather than over the
|
|
2804
|
-
// whole tree, so `--only` bounds the advice as it bounds the
|
|
2805
|
-
// refusals (#2645).
|
|
2806
|
-
for (const message of functionTomlIgnoredKeyWarnings({
|
|
2807
|
-
functionKey,
|
|
2808
|
-
tomlData,
|
|
2809
|
-
})) {
|
|
2810
|
-
warn(` ${message}`);
|
|
2811
|
-
}
|
|
2812
2720
|
}
|
|
2813
2721
|
}
|
|
2814
2722
|
}
|
|
@@ -2868,8 +2776,10 @@ export async function runConfigPushPreflight(input) {
|
|
|
2868
2776
|
}
|
|
2869
2777
|
}
|
|
2870
2778
|
}
|
|
2871
|
-
// #3798 — a
|
|
2872
|
-
//
|
|
2779
|
+
// #3798, #4262 — a prompt's sidecar against its prompt's kind: an agent's
|
|
2780
|
+
// case is checked by the agent test case's shape rule, and `[test.agent]`
|
|
2781
|
+
// on a chat or decisions prompt's case is refused, before any file is
|
|
2782
|
+
// applied.
|
|
2873
2783
|
for (const message of promptAgentSidecarErrors(configDir)) {
|
|
2874
2784
|
preflightValidationErrors.push(` ${message}`);
|
|
2875
2785
|
}
|
|
@@ -3239,10 +3149,74 @@ export function partitionPromptFilesForPush(promptsDir, files) {
|
|
|
3239
3149
|
return { beforeFunctions, afterFunctions };
|
|
3240
3150
|
}
|
|
3241
3151
|
/**
|
|
3242
|
-
*
|
|
3243
|
-
*
|
|
3244
|
-
*
|
|
3245
|
-
*
|
|
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
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* The kind of the prompt a `prompts/<key>.tests/` sidecar belongs to, read
|
|
3188
|
+
* from the prompt's own file — `undefined` for another block type, or when the
|
|
3189
|
+
* file is absent or does not parse (the prompt leg reports that).
|
|
3190
|
+
*/
|
|
3191
|
+
function testCaseOwnerPromptKind(configDir, blockType, blockKey) {
|
|
3192
|
+
if (blockType !== "prompt")
|
|
3193
|
+
return undefined;
|
|
3194
|
+
const owner = join(configDir, "prompts", `${blockKey}.toml`);
|
|
3195
|
+
if (!existsSync(owner))
|
|
3196
|
+
return undefined;
|
|
3197
|
+
try {
|
|
3198
|
+
return promptKindOf(parseTomlFile(owner)?.prompt);
|
|
3199
|
+
}
|
|
3200
|
+
catch {
|
|
3201
|
+
return undefined;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
/** A sidecar's agent block and `expectedJsonSubset`, in wire spelling. */
|
|
3205
|
+
function agentTestCaseWireFields(payload) {
|
|
3206
|
+
const fields = {};
|
|
3207
|
+
for (const key of AGENT_TEST_CASE_FIELDS)
|
|
3208
|
+
fields[key] = payload?.[key];
|
|
3209
|
+
fields.expectedJsonSubset = payload?.expectedJsonSubset;
|
|
3210
|
+
return fields;
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Every `prompts/<key>.tests/*.toml` sidecar the agent test case's rule
|
|
3214
|
+
* refuses — #4262 (#3798 refused every sidecar on an agent). An agent's case
|
|
3215
|
+
* is checked by the vendored shape rule (`resolveAgentTestCase`), which the
|
|
3216
|
+
* admin routes run too; a `[test.agent]` block on a chat or decisions
|
|
3217
|
+
* prompt's case is refused by name. One line per file, naming it. What needs
|
|
3218
|
+
* the app (the input, context and event schemas, the declared names) is the
|
|
3219
|
+
* routes' to decide at apply.
|
|
3246
3220
|
*/
|
|
3247
3221
|
export function promptAgentSidecarErrors(configDir) {
|
|
3248
3222
|
const promptsDir = join(configDir, "prompts");
|
|
@@ -3252,21 +3226,25 @@ export function promptAgentSidecarErrors(configDir) {
|
|
|
3252
3226
|
if (!filePath.startsWith(promptsDir) || !parent.endsWith(".tests"))
|
|
3253
3227
|
continue;
|
|
3254
3228
|
const key = parent.slice(0, -".tests".length);
|
|
3255
|
-
const
|
|
3256
|
-
if (
|
|
3229
|
+
const kind = testCaseOwnerPromptKind(configDir, "prompt", key);
|
|
3230
|
+
if (kind === undefined)
|
|
3257
3231
|
continue;
|
|
3258
|
-
let
|
|
3232
|
+
let sidecar;
|
|
3259
3233
|
try {
|
|
3260
|
-
|
|
3234
|
+
sidecar = parseTomlFile(filePath);
|
|
3261
3235
|
}
|
|
3262
3236
|
catch {
|
|
3237
|
+
// The test-case collector reports a file that does not parse.
|
|
3263
3238
|
continue;
|
|
3264
3239
|
}
|
|
3265
|
-
|
|
3240
|
+
const resolved = resolveAgentTestCase({
|
|
3241
|
+
kind,
|
|
3242
|
+
fields: agentTestCaseWireFields(parseTestCaseToml(sidecar)),
|
|
3243
|
+
});
|
|
3244
|
+
if (!("refusals" in resolved))
|
|
3266
3245
|
continue;
|
|
3267
|
-
const promptKey = tomlData?.prompt?.key || key;
|
|
3268
3246
|
errors.push(`${filePath.slice(configDir.length + 1)}: ` +
|
|
3269
|
-
|
|
3247
|
+
resolved.refusals.map((refusal) => refusal.message).join("; "));
|
|
3270
3248
|
}
|
|
3271
3249
|
return errors;
|
|
3272
3250
|
}
|
|
@@ -3339,18 +3317,12 @@ export function configFileValidationMessages(filePath, tomlData, surface, tables
|
|
|
3339
3317
|
if (surface.label === "prompt") {
|
|
3340
3318
|
messages.push(...promptKindFileErrors(filePath, tomlData));
|
|
3341
3319
|
}
|
|
3342
|
-
// #4116 — a bucket naming its rule set both ways.
|
|
3343
|
-
if (surface.label === "blob-bucket") {
|
|
3344
|
-
messages.push(...blobBucketFileErrors(filePath, tomlData));
|
|
3345
|
-
}
|
|
3346
3320
|
return messages;
|
|
3347
3321
|
}
|
|
3348
3322
|
/**
|
|
3349
3323
|
* Whether `config pull` must rewrite a local file whatever it says (#4160):
|
|
3350
|
-
* `config push` would refuse it
|
|
3351
|
-
*
|
|
3352
|
-
* file because its VALUES match would leave the tree unpushable, or the
|
|
3353
|
-
* deprecation unresolved, after a pull that reported success.
|
|
3324
|
+
* `config push` would refuse it. Keeping such a file because its VALUES match
|
|
3325
|
+
* would leave the tree unpushable after a pull that reported success.
|
|
3354
3326
|
*/
|
|
3355
3327
|
export function pullMustRewrite(label, tomlData) {
|
|
3356
3328
|
// A test-case sidecar is not one of the preflight's per-directory targets;
|
|
@@ -3365,7 +3337,7 @@ export function pullMustRewrite(label, tomlData) {
|
|
|
3365
3337
|
return true;
|
|
3366
3338
|
}
|
|
3367
3339
|
}
|
|
3368
|
-
return
|
|
3340
|
+
return false;
|
|
3369
3341
|
}
|
|
3370
3342
|
export function collectConfigFileValidationErrors(configDir) {
|
|
3371
3343
|
const found = [];
|
|
@@ -3443,35 +3415,6 @@ export function collectFunctionTomlValidationErrors(configDir) {
|
|
|
3443
3415
|
}
|
|
3444
3416
|
return found;
|
|
3445
3417
|
}
|
|
3446
|
-
/**
|
|
3447
|
-
* The retired runtime keys every `functions/*.toml` in the tree still carries
|
|
3448
|
-
* — #3482, the warning half of the collector above.
|
|
3449
|
-
*
|
|
3450
|
-
* Separate from the errors because it IS separate: a file carrying one of
|
|
3451
|
-
* these pushes, with every other key applied, so it is not a row `config diff`
|
|
3452
|
-
* calls invalid. It is a line an author reads on stderr from both commands, so
|
|
3453
|
-
* the two keep reading identically (the #2880 symmetry the errors collector
|
|
3454
|
-
* exists for, applied to advice rather than refusals).
|
|
3455
|
-
*/
|
|
3456
|
-
export function collectFunctionTomlIgnoredKeyWarnings(configDir) {
|
|
3457
|
-
const found = [];
|
|
3458
|
-
const functionsDir = join(configDir, "functions");
|
|
3459
|
-
if (!existsSync(functionsDir))
|
|
3460
|
-
return found;
|
|
3461
|
-
for (const file of readdirSync(functionsDir).filter(isFunctionDeclarationFile)) {
|
|
3462
|
-
const filePath = join(functionsDir, file);
|
|
3463
|
-
let tomlData;
|
|
3464
|
-
try {
|
|
3465
|
-
tomlData = parseTomlFile(filePath);
|
|
3466
|
-
}
|
|
3467
|
-
catch {
|
|
3468
|
-
continue;
|
|
3469
|
-
}
|
|
3470
|
-
const key = tomlData?.function?.key || basename(file, ".toml");
|
|
3471
|
-
found.push(...functionTomlIgnoredKeyWarnings({ functionKey: key, tomlData }));
|
|
3472
|
-
}
|
|
3473
|
-
return found;
|
|
3474
|
-
}
|
|
3475
3418
|
/**
|
|
3476
3419
|
* The collectors' rows, one per diff row (#2880 symmetry, #3320).
|
|
3477
3420
|
*
|
|
@@ -3726,26 +3669,15 @@ export function serializeWorkflow(workflow, draft, configs, logger) {
|
|
|
3726
3669
|
};
|
|
3727
3670
|
return stringifyConfigToml(data);
|
|
3728
3671
|
}
|
|
3729
|
-
//
|
|
3730
|
-
//
|
|
3731
|
-
//
|
|
3732
|
-
//
|
|
3733
|
-
|
|
3734
|
-
// per file — if the existing file uses the legacy JSON-string form, we
|
|
3735
|
-
// preserve it so we don't generate surprise diffs. New files default to
|
|
3736
|
-
// native. The round-trip serializes with `smol-toml` (TOML 1.0), so
|
|
3737
|
-
// heterogeneous arrays emit natively (issue #1450); only shapes that TOML
|
|
3738
|
-
// genuinely can't represent without data loss (null / undefined / an array
|
|
3739
|
-
// containing null) fall back to a JSON string per field with a log message.
|
|
3740
|
-
//
|
|
3741
|
-
// Issue #666: when the type has a stored `schema` (raw TOML of `[models.*]`
|
|
3742
|
-
// blocks), `buildDatabaseTypeTomlData` parses + merges it into the same TOML
|
|
3743
|
-
// object so the result round-trips as `[models.<Name>.fields.<field>]`.
|
|
3744
|
-
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 = {}) {
|
|
3745
3677
|
// #2644 — name any server key this CLI version does not know, rather than
|
|
3746
3678
|
// dropping it silently on the way into the file.
|
|
3747
3679
|
warnUnrecognizedServerKeys(typeConfig, DATABASE_TYPE_TABLE, `database-type-configs/${typeConfig?.databaseType}.toml`, options.logger);
|
|
3748
|
-
const data = buildDatabaseTypeTomlData(typeConfig,
|
|
3680
|
+
const data = buildDatabaseTypeTomlData(typeConfig, ruleSetIdToName);
|
|
3749
3681
|
return stringifyConfigToml(data);
|
|
3750
3682
|
}
|
|
3751
3683
|
/**
|
|
@@ -3965,10 +3897,9 @@ export function dbTypeOwnedScalars(table = DATABASE_TYPE_TABLE) {
|
|
|
3965
3897
|
];
|
|
3966
3898
|
}
|
|
3967
3899
|
export const DB_TYPE_OWNED_SCALARS = dbTypeOwnedScalars();
|
|
3968
|
-
// User-facing TOML key names for the "Cleared <field>" push output
|
|
3969
|
-
// 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.
|
|
3970
3902
|
const DB_TYPE_FIELD_LABELS = {
|
|
3971
|
-
metadataAccess: "celContextAccess",
|
|
3972
3903
|
ruleSetId: "ruleSetName",
|
|
3973
3904
|
};
|
|
3974
3905
|
/** The user-facing TOML key for a wire field (for push output). */
|
|
@@ -4041,13 +3972,6 @@ export function ownedScalarsBeingCleared(updateData, serverConfig) {
|
|
|
4041
3972
|
* still comes from the handler that knows what is wrong with it.
|
|
4042
3973
|
*/
|
|
4043
3974
|
function canonicalizeDatabaseTypeConfig(typeConfig) {
|
|
4044
|
-
// CEL fields: trimmed by the handler, and a whitespace-only expression is
|
|
4045
|
-
// stored as none at all — which the projection reads as unset either way.
|
|
4046
|
-
for (const key of ["defaultAccess", "metadataAccess"]) {
|
|
4047
|
-
if (typeof typeConfig[key] === "string") {
|
|
4048
|
-
typeConfig[key] = typeConfig[key].trim();
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
3975
|
// #750 — `"user.userId"` is shorthand for `{ value: "user.userId", on:
|
|
4052
3976
|
// ["create"] }`, and a scalar `on` for a one-element list.
|
|
4053
3977
|
const auto = typeConfig.autoPopulatedFields;
|
|
@@ -4094,70 +4018,15 @@ function canonicalizeDatabaseTypeConfig(typeConfig) {
|
|
|
4094
4018
|
}
|
|
4095
4019
|
}
|
|
4096
4020
|
}
|
|
4097
|
-
/**
|
|
4098
|
-
* What this push's operation set will BE, stated to the server (issues #813,
|
|
4099
|
-
* #1336, #2732).
|
|
4100
|
-
*
|
|
4101
|
-
* The type-config PATCH runs its gates BEFORE the same push's operation calls,
|
|
4102
|
-
* so without a look-ahead every gate judges the pre-push operations: a push
|
|
4103
|
-
* that removes a model is blocked by the very references it is deleting. The
|
|
4104
|
-
* file IS the target state (config-as-code), so the claims are derived from it:
|
|
4105
|
-
*
|
|
4106
|
-
* - `finalOpNames` — the names the type ends with. Both other claims are
|
|
4107
|
-
* verified against it server-side, which is what keeps them from being a
|
|
4108
|
-
* gate bypass for a direct-API caller.
|
|
4109
|
-
* - `pendingOpDeletes` — its complement among the ops last sync recorded, so
|
|
4110
|
-
* an op this push deletes is excluded from the schema / OPS_EXIST gates.
|
|
4111
|
-
* - `pendingOpUpserts` — the post-push BODY of every declared op, so the
|
|
4112
|
-
* schema-edit gate lints a rewritten op as rewritten and a model removal
|
|
4113
|
-
* plus its operation rewrites lands in one push (#2732).
|
|
4114
|
-
* - `pendingOpUpdates` — the rule-only form (`access`/`params`) the manifest
|
|
4115
|
-
* re-lint reads (#1336).
|
|
4116
|
-
*
|
|
4117
|
-
* All four are built here and sent together, on the dry-run and on the real
|
|
4118
|
-
* PATCH alike. They are never conditioned on the push having a deletion: a push
|
|
4119
|
-
* that only REWRITES operations is exactly the case #2732 exists for.
|
|
4120
|
-
*/
|
|
4121
|
-
export function buildPendingOpClaims(operations, existingOpNames) {
|
|
4122
|
-
const finalOpNames = operations.map((op) => op.name);
|
|
4123
|
-
const declared = new Set(finalOpNames);
|
|
4124
|
-
return {
|
|
4125
|
-
pendingOpDeletes: existingOpNames.filter((n) => !declared.has(n)),
|
|
4126
|
-
finalOpNames,
|
|
4127
|
-
pendingOpUpdates: operations.map((op) => ({
|
|
4128
|
-
name: op.name,
|
|
4129
|
-
access: typeof op.access === "string" ? op.access : null,
|
|
4130
|
-
params: op.params ?? null,
|
|
4131
|
-
})),
|
|
4132
|
-
// The same body the op create/update call sends, so the gate lints what
|
|
4133
|
-
// this push is about to store.
|
|
4134
|
-
pendingOpUpserts: operations.map((op) => ({
|
|
4135
|
-
name: op.name,
|
|
4136
|
-
type: op.type,
|
|
4137
|
-
modelName: op.modelName,
|
|
4138
|
-
access: op.access,
|
|
4139
|
-
definition: op.definition,
|
|
4140
|
-
params: op.params,
|
|
4141
|
-
})),
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
4021
|
export function parseDatabaseTypeToml(tomlData) {
|
|
4145
4022
|
const typeSection = tomlData.type || {};
|
|
4146
4023
|
// #2644 — the `[type]` field list comes from the vendored definition, so a
|
|
4147
4024
|
// scalar added there is read off the file with no edit here. The values the
|
|
4148
4025
|
// handler rewrites before storing — `autoPopulatedFields` (#750), its
|
|
4149
|
-
// `timestamps` sibling (#748)
|
|
4026
|
+
// `timestamps` sibling (#748) — are canonicalized
|
|
4150
4027
|
// below so the comparison reads the spelling the server keeps (#2731 B1).
|
|
4151
4028
|
const typeConfig = dropUndefined(buildPayloadFromToml(DATABASE_TYPE_TABLE, typeSection, {
|
|
4152
4029
|
mode: "create",
|
|
4153
|
-
values: {
|
|
4154
|
-
// Accept both `celContextAccess` (new user-facing key, the definition's
|
|
4155
|
-
// tomlKey) and `metadataAccess` (legacy, declared in `tomlOnlyKeys`).
|
|
4156
|
-
// The newer key wins when both appear. Issue #1567: a defined-check,
|
|
4157
|
-
// not a truthy guard, so `celContextAccess = ""` is honored as an
|
|
4158
|
-
// explicit clear rather than falling through to the legacy key.
|
|
4159
|
-
metadataAccess: (value, _mode, section) => value !== undefined ? value : section?.metadataAccess,
|
|
4160
|
-
},
|
|
4161
4030
|
}));
|
|
4162
4031
|
canonicalizeDatabaseTypeConfig(typeConfig);
|
|
4163
4032
|
// The `ruleSetName` reference; it resolves to an id later.
|
|
@@ -4196,27 +4065,11 @@ export function parseDatabaseTypeToml(tomlData) {
|
|
|
4196
4065
|
}
|
|
4197
4066
|
}
|
|
4198
4067
|
typeConfig.schema = schemaSerialized;
|
|
4199
|
-
// `
|
|
4200
|
-
//
|
|
4201
|
-
//
|
|
4202
|
-
//
|
|
4203
|
-
|
|
4204
|
-
const normalized = normalizeOperationFromToml(op);
|
|
4205
|
-
// #2731 B1 — the handler trims `name` before storing, and the trimmed name
|
|
4206
|
-
// is half the row's key (`<databaseType>#<name>`). Comparing or sending the
|
|
4207
|
-
// padded spelling names a row the server never holds: the child reconcile
|
|
4208
|
-
// would read it as a different operation and plan it forever.
|
|
4209
|
-
if (typeof normalized.name === "string") {
|
|
4210
|
-
normalized.name = normalized.name.trim();
|
|
4211
|
-
}
|
|
4212
|
-
return normalized;
|
|
4213
|
-
});
|
|
4214
|
-
// Subscriptions (issue #803): `[[subscriptions]]` array-of-tables.
|
|
4215
|
-
// `normalizeSubscriptionFromToml` maps `accessRule`→`access` (throwing on a
|
|
4216
|
-
// conflicting both-present declaration) and forwards `select`/`emit`/`params`
|
|
4217
|
-
// in the wire shape. Until #803 this block was dropped at parse time.
|
|
4218
|
-
const subscriptions = (tomlData.subscriptions || []).map((sub) => normalizeSubscriptionFromToml(sub));
|
|
4219
|
-
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 };
|
|
4220
4073
|
}
|
|
4221
4074
|
/**
|
|
4222
4075
|
* The parsed shape of a rule-set TOML: the definition's fields plus the
|
|
@@ -5001,12 +4854,11 @@ const CONFIG_DIFF_SPECS = {
|
|
|
5001
4854
|
keys.push("name");
|
|
5002
4855
|
if (!bucket.preset)
|
|
5003
4856
|
keys.push("preset");
|
|
5004
|
-
//
|
|
5005
|
-
//
|
|
5006
|
-
//
|
|
4857
|
+
// A name manages the rule set (#4116) — read as the parse reads it, so a
|
|
4858
|
+
// blank name is no reference rather than a difference the update cannot
|
|
4859
|
+
// carry. The retired id spelling (#4283) manages nothing.
|
|
5007
4860
|
const named = typeof bucket.ruleSetName === "string" && bucket.ruleSetName.trim() !== "";
|
|
5008
|
-
|
|
5009
|
-
if (!named && !legacy)
|
|
4861
|
+
if (!named)
|
|
5010
4862
|
keys.push("ruleSetId");
|
|
5011
4863
|
return keys;
|
|
5012
4864
|
},
|
|
@@ -5045,29 +4897,17 @@ const CONFIG_DIFF_SPECS = {
|
|
|
5045
4897
|
"database-type-config": {
|
|
5046
4898
|
label: "database-type-config",
|
|
5047
4899
|
table: DATABASE_TYPE_TABLE,
|
|
5048
|
-
parse: (doc) =>
|
|
5049
|
-
|
|
5050
|
-
// The three sub-trees ride ON the type config, so they travel with it and
|
|
5051
|
-
// are pulled back out as extras below.
|
|
5052
|
-
typeConfig.__operations = operations;
|
|
5053
|
-
typeConfig.__subscriptions = subscriptions;
|
|
5054
|
-
return typeConfig;
|
|
5055
|
-
},
|
|
5056
|
-
serialize: (record, maps, extra) => serializeDatabaseType(record, extra?.operations ?? [], maps.ruleSetIdToName, {
|
|
5057
|
-
defaultForm: "native",
|
|
5058
|
-
subscriptions: extra?.subscriptions ?? [],
|
|
5059
|
-
}),
|
|
4900
|
+
parse: (doc) => parseDatabaseTypeToml(doc).typeConfig,
|
|
4901
|
+
serialize: (record, maps) => serializeDatabaseType(record, maps.ruleSetIdToName),
|
|
5060
4902
|
extras: (entity) => ({
|
|
5061
4903
|
// Structural, per the definition: the `[models.*]` schema, the
|
|
5062
|
-
// `[triggers]` tree
|
|
4904
|
+
// `[triggers]` tree and the manifest. (#3189 — the operation and
|
|
4905
|
+
// subscription rows that used to ride beside them are retired.)
|
|
5063
4906
|
schema: entity.schema ?? null,
|
|
5064
4907
|
triggers: entity.triggers ?? null,
|
|
5065
4908
|
metadataManifest: entity.metadataManifest ?? null,
|
|
5066
|
-
operations: byKey(entity.__operations, "name"),
|
|
5067
|
-
subscriptions: byKey(entity.__subscriptions, "subscriptionKey"),
|
|
5068
4909
|
_unresolvedRuleSetName: entity._unresolvedRuleSetName,
|
|
5069
4910
|
}),
|
|
5070
|
-
childKeys: { operations: "name", subscriptions: "subscriptionKey" },
|
|
5071
4911
|
tomlFields: ["schema"],
|
|
5072
4912
|
ruleSetRef: true,
|
|
5073
4913
|
describe: (entity) => `database type "${entity?.databaseType}"`,
|
|
@@ -5406,36 +5246,6 @@ export function withDerivedIdentity(doc, tomlPath, identity) {
|
|
|
5406
5246
|
export function resolveExistingId(input) {
|
|
5407
5247
|
return input.liveOk ? input.liveId : input.manifestId;
|
|
5408
5248
|
}
|
|
5409
|
-
/**
|
|
5410
|
-
* Which CHILD rows a database type's push reconciles against (#2731 B3).
|
|
5411
|
-
*
|
|
5412
|
-
* `resolveExistingId` answers this for a whole entity; a database type's
|
|
5413
|
-
* operations and subscriptions are rows with the same question and the same
|
|
5414
|
-
* two possible answers. The manifest can name an operation the server no
|
|
5415
|
-
* longer has (the update PUTs a row that is gone) and be silent about one it
|
|
5416
|
-
* does have (the create POSTs a duplicate key) — both abort the push with a
|
|
5417
|
-
* server error, which is exactly the recovery `--force` is supposed to
|
|
5418
|
-
* perform.
|
|
5419
|
-
*
|
|
5420
|
-
* When the reconcile read the rows, they ARE the baseline: existence, the
|
|
5421
|
-
* timestamp each update guards with, and — as the complement of the file's own
|
|
5422
|
-
* list — which rows this push deletes. `undefined` means the read did not
|
|
5423
|
-
* happen (the degraded path, or a type live state says is absent), and the
|
|
5424
|
-
* manifest stays what it has always been.
|
|
5425
|
-
*/
|
|
5426
|
-
export function liveChildBaseline(live, manifestChildren) {
|
|
5427
|
-
if (!live)
|
|
5428
|
-
return undefined;
|
|
5429
|
-
const baseline = {};
|
|
5430
|
-
for (const [key, row] of live) {
|
|
5431
|
-
baseline[key] = {
|
|
5432
|
-
// The live timestamp is the one an update guards with; the manifest's is
|
|
5433
|
-
// a stand-in only for a row the listing returned without one.
|
|
5434
|
-
modifiedAt: row?.modifiedAt || manifestChildren?.[key]?.modifiedAt || "",
|
|
5435
|
-
};
|
|
5436
|
-
}
|
|
5437
|
-
return baseline;
|
|
5438
|
-
}
|
|
5439
5249
|
/**
|
|
5440
5250
|
* The app's live rule sets, indexed the three ways push reads them (#2731 B3).
|
|
5441
5251
|
*
|
|
@@ -5974,6 +5784,17 @@ function testCaseSidecarTable(testCase, lookupMaps, unresolvedSpelling) {
|
|
|
5974
5784
|
expectedOutputPattern: (record) => record.expectedOutputPattern || "",
|
|
5975
5785
|
expectedOutputContains: (record) => jsonFieldText(record.expectedOutputContains, "[]"),
|
|
5976
5786
|
expectedJsonSubset: (record) => jsonFieldText(record.expectedJsonSubset, "{}"),
|
|
5787
|
+
// #4262 — `[test.agent]`, written only when a key is set, so a chat case's
|
|
5788
|
+
// sidecar is the one it always was. The history is the text the author
|
|
5789
|
+
// pushed; the two JSON fields come back parsed and are written as text.
|
|
5790
|
+
agentHistory: (record) => agentJsonText(record.agentHistory),
|
|
5791
|
+
agentContext: (record) => agentJsonText(record.agentContext),
|
|
5792
|
+
agentTimezone: (record) => record.agentTimezone || undefined,
|
|
5793
|
+
agentLocale: (record) => record.agentLocale || undefined,
|
|
5794
|
+
agentExpectedToolCall: (record) => agentJsonText(record.agentExpectedToolCall),
|
|
5795
|
+
agentExpectedNoToolCall: (record) => typeof record.agentExpectedNoToolCall === "boolean"
|
|
5796
|
+
? record.agentExpectedNoToolCall
|
|
5797
|
+
: undefined,
|
|
5977
5798
|
});
|
|
5978
5799
|
return { test, unresolved };
|
|
5979
5800
|
}
|
|
@@ -6028,6 +5849,38 @@ function jsonFieldText(value, empty) {
|
|
|
6028
5849
|
return empty;
|
|
6029
5850
|
}
|
|
6030
5851
|
}
|
|
5852
|
+
/**
|
|
5853
|
+
* #4262 — an agent block field as the sidecar's JSON text, or `undefined`
|
|
5854
|
+
* (omitted) when unset: `[test.agent]` has no empty spellings.
|
|
5855
|
+
*/
|
|
5856
|
+
function agentJsonText(value) {
|
|
5857
|
+
if (value === undefined || value === null)
|
|
5858
|
+
return undefined;
|
|
5859
|
+
if (typeof value === "string")
|
|
5860
|
+
return value === "" ? undefined : value;
|
|
5861
|
+
try {
|
|
5862
|
+
return JSON.stringify(value);
|
|
5863
|
+
}
|
|
5864
|
+
catch {
|
|
5865
|
+
return undefined;
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
/**
|
|
5869
|
+
* #4262 — an agent block JSON field on the wire: parsed when the text parses,
|
|
5870
|
+
* the text itself otherwise (the shape rule names it), `undefined` when unset.
|
|
5871
|
+
*/
|
|
5872
|
+
function agentJsonWireValue(value) {
|
|
5873
|
+
if (value === undefined || value === null || value === "")
|
|
5874
|
+
return undefined;
|
|
5875
|
+
if (typeof value !== "string")
|
|
5876
|
+
return value;
|
|
5877
|
+
try {
|
|
5878
|
+
return JSON.parse(value);
|
|
5879
|
+
}
|
|
5880
|
+
catch {
|
|
5881
|
+
return value;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
6031
5884
|
export function parseTestCaseToml(tomlData) {
|
|
6032
5885
|
const test = tomlData.test || {};
|
|
6033
5886
|
/** A JSON-string TOML value, or `undefined` when it is absent/the default. */
|
|
@@ -6068,6 +5921,15 @@ export function parseTestCaseToml(tomlData) {
|
|
|
6068
5921
|
expectedOutputPattern: (value) => (value ? value : undefined),
|
|
6069
5922
|
expectedOutputContains: (value) => parseJsonText(value, ["[]"]),
|
|
6070
5923
|
expectedJsonSubset: (value) => parseJsonText(value, ["{}"]),
|
|
5924
|
+
// #4262 — `[test.agent]`. The history travels as the JSON text it is
|
|
5925
|
+
// authored as; context and expectedToolCall travel parsed; `false` is a
|
|
5926
|
+
// value, not an absence.
|
|
5927
|
+
agentHistory: (value) => (value === "" ? undefined : value),
|
|
5928
|
+
agentContext: (value) => agentJsonWireValue(value),
|
|
5929
|
+
agentTimezone: (value) => (value === "" ? undefined : value),
|
|
5930
|
+
agentLocale: (value) => (value === "" ? undefined : value),
|
|
5931
|
+
agentExpectedToolCall: (value) => agentJsonWireValue(value),
|
|
5932
|
+
agentExpectedNoToolCall: (value) => value,
|
|
6071
5933
|
},
|
|
6072
5934
|
});
|
|
6073
5935
|
// An omitted field must be absent from the body, not present-and-undefined:
|
|
@@ -7293,6 +7155,15 @@ export async function pushTestCasesForBlock(params) {
|
|
|
7293
7155
|
evaluatorPromptId: testPayload.evaluatorPromptId ?? null,
|
|
7294
7156
|
evaluatorConfigId: testPayload.evaluatorConfigId ?? null,
|
|
7295
7157
|
};
|
|
7158
|
+
// #4262 — an agent prompt's case carries its `[test.agent]` block as
|
|
7159
|
+
// full state too: a key the file no longer sets goes as `null`, so
|
|
7160
|
+
// removing an assertion clears it. A chat or decisions prompt's case
|
|
7161
|
+
// sends none, so its body is the one it always was.
|
|
7162
|
+
if (testCaseOwnerPromptKind(configDir, blockType, blockKey) === "agent") {
|
|
7163
|
+
for (const key of AGENT_TEST_CASE_FIELDS) {
|
|
7164
|
+
updatePayload[key] = testPayload[key] ?? null;
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7296
7167
|
// #2896 — the identity travels with the content: this is the lazy
|
|
7297
7168
|
// migration of a legacy case and the second half of a rename.
|
|
7298
7169
|
if (pendingKey !== undefined)
|
|
@@ -8318,30 +8189,10 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8318
8189
|
const metadataCategoryConfigsResult = metadataCategoryConfigsOutcome.ok
|
|
8319
8190
|
? metadataCategoryConfigsOutcome.value
|
|
8320
8191
|
: [];
|
|
8321
|
-
//
|
|
8322
|
-
// subscriptions
|
|
8323
|
-
//
|
|
8324
|
-
|
|
8325
|
-
//
|
|
8326
|
-
// A failed operation/subscription fetch also disqualifies the type from
|
|
8327
|
-
// pruning (#1659): the file we write for it is then incomplete, so the
|
|
8328
|
-
// pull's picture of that type is not one to reconcile against.
|
|
8329
|
-
let databaseTypeDetailsOk = true;
|
|
8330
|
-
const databaseTypesWithOps = await Promise.all(selectPull("database-type-config", Array.isArray(databaseTypeConfigsResult) ? databaseTypeConfigsResult : [], (t) => t.databaseType).map(async (typeConfig) => {
|
|
8331
|
-
const [opsOutcome, subsOutcome] = await Promise.all([
|
|
8332
|
-
attemptFetch(() => client.listDatabaseTypeOperations(resolvedAppId, typeConfig.databaseType)),
|
|
8333
|
-
attemptFetch(() => client.listDatabaseTypeSubscriptions(resolvedAppId, typeConfig.databaseType)),
|
|
8334
|
-
]);
|
|
8335
|
-
if (!opsOutcome.ok || !subsOutcome.ok)
|
|
8336
|
-
databaseTypeDetailsOk = false;
|
|
8337
|
-
const ops = opsOutcome.ok ? opsOutcome.value : [];
|
|
8338
|
-
const subs = subsOutcome.ok ? subsOutcome.value : [];
|
|
8339
|
-
return {
|
|
8340
|
-
typeConfig,
|
|
8341
|
-
operations: Array.isArray(ops) ? ops : [],
|
|
8342
|
-
subscriptions: Array.isArray(subs) ? subs : [],
|
|
8343
|
-
};
|
|
8344
|
-
}));
|
|
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);
|
|
8345
8196
|
// Snapshot the pre-pull sync tree BEFORE we touch any local file
|
|
8346
8197
|
// (issue #578, Phase 1). All server data has been fetched and
|
|
8347
8198
|
// validated at this point — a failed pull already threw above, so we
|
|
@@ -8937,46 +8788,16 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8937
8788
|
const ruleSets = selectPull("rule-set", Array.isArray(ruleSetsResult) ? ruleSetsResult : [], (rs) => ruleSetFileKey(rs));
|
|
8938
8789
|
// Write database types
|
|
8939
8790
|
const databaseTypeEntities = {};
|
|
8940
|
-
for (const
|
|
8791
|
+
for (const typeConfig of databaseTypesToWrite) {
|
|
8941
8792
|
const filename = `${typeConfig.databaseType}.toml`;
|
|
8942
8793
|
const filePath = join(configDir, "database-type-configs", filename);
|
|
8943
|
-
|
|
8944
|
-
// file already exists, read it raw and detect which operations
|
|
8945
|
-
// currently use JSON-string vs native nested-table form.
|
|
8946
|
-
const hints = existsSync(filePath)
|
|
8947
|
-
? detectExistingOperationForms(readFileSync(filePath, "utf-8"))
|
|
8948
|
-
: undefined;
|
|
8949
|
-
const content = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
8950
|
-
hints,
|
|
8951
|
-
defaultForm: "native",
|
|
8794
|
+
const content = serializeDatabaseType(typeConfig, ruleSetIdToName, {
|
|
8952
8795
|
logger: (msg) => info(` ${msg}`),
|
|
8953
|
-
// Issue #803: emit `[[subscriptions]]` blocks so a pull → push
|
|
8954
|
-
// cycle round-trips subscriptions without a spurious diff.
|
|
8955
|
-
subscriptions,
|
|
8956
8796
|
});
|
|
8957
8797
|
const written = writePulledFile(filePath, content, pullWrites, sameConfigAs("database-type-config", content, filePath, pullDiffMaps));
|
|
8958
|
-
const opsEntities = {};
|
|
8959
|
-
for (const op of operations) {
|
|
8960
|
-
opsEntities[op.name] = {
|
|
8961
|
-
modifiedAt: op.modifiedAt || new Date().toISOString(),
|
|
8962
|
-
};
|
|
8963
|
-
}
|
|
8964
|
-
// Track per-subscription modifiedAt in SyncState (issue #803),
|
|
8965
|
-
// exactly like operations, so the next push skips unchanged
|
|
8966
|
-
// subscriptions and detects removals.
|
|
8967
|
-
const subsEntities = {};
|
|
8968
|
-
for (const sub of subscriptions || []) {
|
|
8969
|
-
if (!sub?.subscriptionKey)
|
|
8970
|
-
continue;
|
|
8971
|
-
subsEntities[sub.subscriptionKey] = {
|
|
8972
|
-
modifiedAt: sub.modifiedAt || new Date().toISOString(),
|
|
8973
|
-
};
|
|
8974
|
-
}
|
|
8975
8798
|
databaseTypeEntities[typeConfig.databaseType] = {
|
|
8976
8799
|
databaseType: typeConfig.databaseType,
|
|
8977
8800
|
modifiedAt: typeConfig.modifiedAt || new Date().toISOString(),
|
|
8978
|
-
operations: Object.keys(opsEntities).length > 0 ? opsEntities : undefined,
|
|
8979
|
-
subscriptions: Object.keys(subsEntities).length > 0 ? subsEntities : undefined,
|
|
8980
8801
|
contentHash: computeFileHash(filePath),
|
|
8981
8802
|
semanticHash: pulledSemanticHash("database-type-config", content, pullDiffMaps),
|
|
8982
8803
|
hasSchema: typeof typeConfig.schema === "string" &&
|
|
@@ -9101,8 +8922,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9101
8922
|
scripts: scriptPresence,
|
|
9102
8923
|
functions: functionPresence,
|
|
9103
8924
|
emailTemplates: emailTemplatePresence,
|
|
9104
|
-
databaseTypes: databaseTypeConfigsOutcome.ok
|
|
9105
|
-
? { ok: true, serverKeys:
|
|
8925
|
+
databaseTypes: databaseTypeConfigsOutcome.ok
|
|
8926
|
+
? { ok: true, serverKeys: databaseTypesToWrite.map((t) => t.databaseType) }
|
|
9106
8927
|
: { ok: false },
|
|
9107
8928
|
ruleSets: ruleSetsOutcome.ok
|
|
9108
8929
|
? { ok: true, serverKeys: ruleSets.map((rs) => ruleSetFileKey(rs)) }
|
|
@@ -9462,7 +9283,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9462
9283
|
if (unwrittenTestCases > 0) {
|
|
9463
9284
|
keyValue("Test Cases not written", unwrittenTestCases);
|
|
9464
9285
|
}
|
|
9465
|
-
keyValue("Database Types",
|
|
9286
|
+
keyValue("Database Types", databaseTypesToWrite.length);
|
|
9466
9287
|
keyValue("Rule Sets", ruleSets.length);
|
|
9467
9288
|
keyValue("Group Type Configs", groupTypeConfigs.length);
|
|
9468
9289
|
keyValue("Collection Type Configs", collectionTypeConfigs.length);
|
|
@@ -11784,16 +11605,6 @@ What a push guarantees (two stages):
|
|
|
11784
11605
|
catch (err) {
|
|
11785
11606
|
throw new Error(`${filePath}: ${err.message}`);
|
|
11786
11607
|
}
|
|
11787
|
-
// #2652 — a prompt with no rule denies every non-admin execution.
|
|
11788
|
-
// Warn per file so an operator pushing an unmigrated app can see
|
|
11789
|
-
// exactly which prompts are locked down and how to open them.
|
|
11790
|
-
const promptRuleWarning = missingAccessRuleWarning({
|
|
11791
|
-
kind: "prompt",
|
|
11792
|
-
key,
|
|
11793
|
-
accessRule: prompt.accessRule,
|
|
11794
|
-
});
|
|
11795
|
-
if (promptRuleWarning)
|
|
11796
|
-
warn(` ${promptRuleWarning}`);
|
|
11797
11608
|
const existingEntry = syncState?.entities?.prompts?.[key];
|
|
11798
11609
|
const livePromptId = livePromptIds.get(key);
|
|
11799
11610
|
// A detail read that fails degrades THIS prompt to the byte-hash
|
|
@@ -12808,7 +12619,6 @@ What a push guarantees (two stages):
|
|
|
12808
12619
|
// start from this release onward. A `runAs = "system"` workflow is
|
|
12809
12620
|
// exempt (it rejects rules and members cannot start it).
|
|
12810
12621
|
const workflowRuleWarning = missingAccessRuleWarning({
|
|
12811
|
-
kind: "workflow",
|
|
12812
12622
|
key,
|
|
12813
12623
|
accessRule: workflow.accessRule,
|
|
12814
12624
|
runAs: workflow.runAs,
|
|
@@ -13260,12 +13070,11 @@ What a push guarantees (two stages):
|
|
|
13260
13070
|
const dbTypesDir = join(configDir, "database-type-configs");
|
|
13261
13071
|
if (existsSync(dbTypesDir)) {
|
|
13262
13072
|
const files = selectFiles("database-type-config", dbTypesDir, readdirSync(dbTypesDir).filter((f) => f.endsWith(".toml")));
|
|
13263
|
-
// #2731 B3 — live reconcile for the FILE-LEVEL gate
|
|
13264
|
-
//
|
|
13265
|
-
//
|
|
13266
|
-
//
|
|
13267
|
-
//
|
|
13268
|
-
// 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.)
|
|
13269
13078
|
const databaseTypeSpec = configDiffSpec("database-type-config");
|
|
13270
13079
|
const liveDatabaseTypes = new Map();
|
|
13271
13080
|
let databaseTypesLiveOk = (await loadLiveRuleSets()).ok;
|
|
@@ -13287,28 +13096,8 @@ What a push guarantees (two stages):
|
|
|
13287
13096
|
}
|
|
13288
13097
|
for (const file of files) {
|
|
13289
13098
|
const filePath = join(dbTypesDir, file);
|
|
13290
|
-
const rawToml = readFileSync(filePath, "utf-8");
|
|
13291
13099
|
const tomlData = parseTomlFile(filePath);
|
|
13292
|
-
|
|
13293
|
-
let operations;
|
|
13294
|
-
let subscriptions;
|
|
13295
|
-
try {
|
|
13296
|
-
({ typeConfig, operations, subscriptions } =
|
|
13297
|
-
parseDatabaseTypeToml(tomlData));
|
|
13298
|
-
}
|
|
13299
|
-
catch (err) {
|
|
13300
|
-
// The only structured parse error today is the
|
|
13301
|
-
// `accessRule`/`access` conflict in a `[[subscriptions]]` block
|
|
13302
|
-
// (issue #803). Surface it with the file name and abort the
|
|
13303
|
-
// push rather than crashing with an opaque stack.
|
|
13304
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
13305
|
-
// Issue #976: throw (not `process.exit`) so the save-on-failure
|
|
13306
|
-
// catch is the only exit path. Validation is also front-loaded
|
|
13307
|
-
// above, so this is defense-in-depth.
|
|
13308
|
-
throw new Error(` ${file}: ${err.message}\nAborting push: subscription \`access\`/\`accessRule\` conflict in ${file}.`);
|
|
13309
|
-
}
|
|
13310
|
-
throw err;
|
|
13311
|
-
}
|
|
13100
|
+
const { typeConfig } = parseDatabaseTypeToml(tomlData);
|
|
13312
13101
|
const dbType = typeConfig.databaseType || basename(file, ".toml");
|
|
13313
13102
|
// The gate re-parses this document, so the name-derived identity
|
|
13314
13103
|
// has to be in it (see `withDerivedIdentity`).
|
|
@@ -13317,63 +13106,21 @@ What a push guarantees (two stages):
|
|
|
13317
13106
|
resolveRuleSetReference(typeConfig, ruleSetNameToId, `database type ${dbType}`, {
|
|
13318
13107
|
hint: ruleSetSelectionHint,
|
|
13319
13108
|
});
|
|
13320
|
-
// $params validator (issue #752): every `$params.X` reference
|
|
13321
|
-
// inside `definition` must correspond to a declared param in
|
|
13322
|
-
// `[[operations.params]]`. Catches typos like `$params.proectId`
|
|
13323
|
-
// at push time with the TOML file:line of the offending op block.
|
|
13324
|
-
const validation = validateOperations({
|
|
13325
|
-
filePath,
|
|
13326
|
-
rawToml,
|
|
13327
|
-
operations,
|
|
13328
|
-
});
|
|
13329
|
-
for (const w of validation.warnings) {
|
|
13330
|
-
warn(` ${formatIssue(w)}`);
|
|
13331
|
-
}
|
|
13332
|
-
if (validation.errors.length > 0) {
|
|
13333
|
-
// Issue #976: throw (not `process.exit`) so the save-on-failure
|
|
13334
|
-
// catch is the only exit path. Validation is also front-loaded
|
|
13335
|
-
// above, so this is defense-in-depth.
|
|
13336
|
-
throw new Error(validation.errors.map((e) => ` ${formatIssue(e)}`).join("\n") +
|
|
13337
|
-
`\nAborting push: ${validation.errors.length} unresolved $params reference(s) in ${file}.`);
|
|
13338
|
-
}
|
|
13339
13109
|
const existingEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
13340
13110
|
/**
|
|
13341
|
-
* This type as the server holds it NOW
|
|
13342
|
-
* subscriptions, which the comparison treats as one unit.
|
|
13111
|
+
* This type as the server holds it NOW.
|
|
13343
13112
|
*
|
|
13344
13113
|
* Two callers: the baseline stamped after everything this file
|
|
13345
13114
|
* writes has landed (B4), and the report for a 409 that arrived
|
|
13346
13115
|
* after the reconcile fetch (B5).
|
|
13347
13116
|
*/
|
|
13348
13117
|
const refetchLiveDatabaseType = async () => {
|
|
13349
|
-
const
|
|
13350
|
-
|
|
13351
|
-
client.listDatabaseTypeOperations(resolvedAppId, dbType),
|
|
13352
|
-
client.listDatabaseTypeSubscriptions(resolvedAppId, dbType),
|
|
13353
|
-
]);
|
|
13354
|
-
return record
|
|
13355
|
-
? {
|
|
13356
|
-
record,
|
|
13357
|
-
extra: {
|
|
13358
|
-
operations: Array.isArray(ops) ? ops : [],
|
|
13359
|
-
subscriptions: Array.isArray(subs) ? subs : [],
|
|
13360
|
-
},
|
|
13361
|
-
}
|
|
13362
|
-
: null;
|
|
13118
|
+
const record = await client.getDatabaseTypeConfig(resolvedAppId, dbType);
|
|
13119
|
+
return record ? { record, extra: {} } : null;
|
|
13363
13120
|
};
|
|
13364
|
-
// Issue #915 (defect b): track whether the apply path rejected any
|
|
13365
|
-
// op for THIS file via the schema gate (OperationRefError /
|
|
13366
|
-
// SchemaRequiredError). On a non-transactional fresh-type push the
|
|
13367
|
-
// db-type lands but the bad op is blocked and the loop continues; if
|
|
13368
|
-
// we then wrote the file-level contentHash, the next push's
|
|
13369
|
-
// `shouldPushFile` would short-circuit and the missing op would be
|
|
13370
|
-
// invisible drift. When this flag is set we skip the contentHash
|
|
13371
|
-
// write so the drift stays visible and a corrected re-push converges.
|
|
13372
|
-
let fileHadGateRejectedOp = false;
|
|
13373
13121
|
// #2731 B4: a 409 means the write was REJECTED, so this file did
|
|
13374
|
-
// not land
|
|
13375
|
-
//
|
|
13376
|
-
// 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
|
|
13377
13124
|
// still-unapplied local edit as server drift and refuse to retry.
|
|
13378
13125
|
let fileHadConflict = false;
|
|
13379
13126
|
const liveDatabaseType = databaseTypesLiveOk
|
|
@@ -13386,9 +13133,6 @@ What a push guarantees (two stages):
|
|
|
13386
13133
|
const typeExistsOnServer = databaseTypesLiveOk
|
|
13387
13134
|
? !!liveDatabaseType
|
|
13388
13135
|
: !!existingEntry;
|
|
13389
|
-
/** Live operations/subscriptions, keyed by name, when read. */
|
|
13390
|
-
let liveOpsByName;
|
|
13391
|
-
let liveSubsByName;
|
|
13392
13136
|
let dbTypeGate;
|
|
13393
13137
|
if (databaseTypesLiveOk && !liveDatabaseType) {
|
|
13394
13138
|
// The listing succeeded and this type is not in it. That is a
|
|
@@ -13397,87 +13141,47 @@ What a push guarantees (two stages):
|
|
|
13397
13141
|
dbTypeGate = { action: "create" };
|
|
13398
13142
|
}
|
|
13399
13143
|
else if (databaseTypesLiveOk && liveDatabaseType) {
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
.
|
|
13421
|
-
|
|
13422
|
-
dbTypeGate = decidePushForConfig({
|
|
13423
|
-
spec: databaseTypeSpec,
|
|
13424
|
-
maps: pushDiffMaps,
|
|
13425
|
-
localParsed: tomlData,
|
|
13426
|
-
live: {
|
|
13427
|
-
record: liveDatabaseType,
|
|
13428
|
-
extra: {
|
|
13429
|
-
operations: Array.isArray(liveOps) ? liveOps : [],
|
|
13430
|
-
subscriptions: Array.isArray(liveSubs) ? liveSubs : [],
|
|
13431
|
-
},
|
|
13432
|
-
modifiedAt: liveDatabaseType.modifiedAt,
|
|
13433
|
-
},
|
|
13434
|
-
entry: existingEntry,
|
|
13435
|
-
localFileHash: computeFileHash(filePath),
|
|
13436
|
-
force: options.force,
|
|
13437
|
-
});
|
|
13438
|
-
if (dbTypeGate.action === "skip") {
|
|
13439
|
-
skippedCount++;
|
|
13440
|
-
if (syncState) {
|
|
13441
|
-
// Written even when the manifest has never seen this type:
|
|
13442
|
-
// an adopt that records nothing is repeated on every push,
|
|
13443
|
-
// and its next local edit would have no baseline (and no
|
|
13444
|
-
// op/subscription rows) to reconcile against.
|
|
13445
|
-
if (!syncState.entities.databaseTypes) {
|
|
13446
|
-
syncState.entities.databaseTypes = {};
|
|
13447
|
-
}
|
|
13448
|
-
const opsEntities = {};
|
|
13449
|
-
for (const [opName, op] of liveOpsByName ?? []) {
|
|
13450
|
-
opsEntities[opName] = {
|
|
13451
|
-
modifiedAt: op?.modifiedAt || new Date().toISOString(),
|
|
13452
|
-
};
|
|
13453
|
-
}
|
|
13454
|
-
const subsEntities = {};
|
|
13455
|
-
for (const [subKey, sub] of liveSubsByName ?? []) {
|
|
13456
|
-
subsEntities[subKey] = {
|
|
13457
|
-
modifiedAt: sub?.modifiedAt || new Date().toISOString(),
|
|
13458
|
-
};
|
|
13459
|
-
}
|
|
13460
|
-
syncState.entities.databaseTypes[dbType] = {
|
|
13461
|
-
databaseType: dbType,
|
|
13462
|
-
modifiedAt: liveDatabaseType.modifiedAt ||
|
|
13463
|
-
existingEntry?.modifiedAt ||
|
|
13464
|
-
new Date().toISOString(),
|
|
13465
|
-
operations: Object.keys(opsEntities).length > 0 ? opsEntities : undefined,
|
|
13466
|
-
subscriptions: Object.keys(subsEntities).length > 0 ? subsEntities : undefined,
|
|
13467
|
-
contentHash: computeFileHash(filePath),
|
|
13468
|
-
semanticHash: dbTypeGate.remoteHash,
|
|
13469
|
-
hasSchema: typeof liveDatabaseType.schema === "string" &&
|
|
13470
|
-
liveDatabaseType.schema.trim().length > 0,
|
|
13471
|
-
};
|
|
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 = {};
|
|
13472
13166
|
}
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
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
|
+
};
|
|
13480
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;
|
|
13481
13185
|
}
|
|
13482
13186
|
}
|
|
13483
13187
|
// Fallback gate: the manifest byte hash — and, since #2880, a
|
|
@@ -13497,26 +13201,6 @@ What a push guarantees (two stages):
|
|
|
13497
13201
|
continue;
|
|
13498
13202
|
}
|
|
13499
13203
|
}
|
|
13500
|
-
// ── Issues #813 / #1336 / #2732: state what this push's operation
|
|
13501
|
-
// set will BE, then run a validate-first pass before any mutating
|
|
13502
|
-
// call for this type. The type-config PATCH gates run ahead of the
|
|
13503
|
-
// operation calls, so the claims are what let them judge the
|
|
13504
|
-
// post-push state (see `buildPendingOpClaims`). ──
|
|
13505
|
-
// The op rows the server holds (#2731). When the reconcile read
|
|
13506
|
-
// them, they ARE the inventory (`liveChildBaseline`): an op created
|
|
13507
|
-
// out-of-band is UPDATEd rather than POSTed into a duplicate-key
|
|
13508
|
-
// failure, and one deleted out-of-band is re-CREATEd rather than
|
|
13509
|
-
// PATCHed at a row that is gone — and the claims' delete complement
|
|
13510
|
-
// is computed against that live inventory, not the manifest's. When
|
|
13511
|
-
// live state says the TYPE is gone, its ops went with it — so the
|
|
13512
|
-
// manifest's op list is not a baseline either, and this push
|
|
13513
|
-
// creates them alongside the re-created type. Only the degraded
|
|
13514
|
-
// path still reads the manifest, where it is what the byte-hash
|
|
13515
|
-
// gate has always used.
|
|
13516
|
-
const existingOpsForType = liveChildBaseline(liveOpsByName, existingEntry?.operations) ??
|
|
13517
|
-
((typeExistsOnServer ? existingEntry?.operations : undefined) || {});
|
|
13518
|
-
const pendingOpClaims = buildPendingOpClaims(operations, Object.keys(existingOpsForType));
|
|
13519
|
-
const { pendingOpDeletes } = pendingOpClaims;
|
|
13520
13204
|
// Whether the SERVER holds a schema right now. Live state answers
|
|
13521
13205
|
// when it was read (so an adopted type's schema is not silently
|
|
13522
13206
|
// preserved by an omitted key); the manifest's `hasSchema` is the
|
|
@@ -13575,8 +13259,9 @@ What a push guarantees (two stages):
|
|
|
13575
13259
|
// declares an owned scalar (matches the pre-#1567 PATCH trigger), a
|
|
13576
13260
|
// schema set/delete is in the payload, or an owned scalar is being
|
|
13577
13261
|
// cleared. When false, the type-config PATCH is skipped entirely so
|
|
13578
|
-
// an
|
|
13579
|
-
// "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.
|
|
13580
13265
|
const declaresOwnedField = DB_TYPE_OWNED_SCALARS.some((k) => k in typeConfig);
|
|
13581
13266
|
const schemaInUpdateData = "schema" in computeTypeUpdateData();
|
|
13582
13267
|
const typeLevelChange = declaresOwnedField ||
|
|
@@ -13588,19 +13273,12 @@ What a push guarantees (two stages):
|
|
|
13588
13273
|
info(` ${prefix}${dbTypeFieldLabel(key)} on ${dbType}`);
|
|
13589
13274
|
}
|
|
13590
13275
|
};
|
|
13591
|
-
// Validate-first pass (decision 4a): run
|
|
13592
|
-
//
|
|
13593
|
-
//
|
|
13594
|
-
//
|
|
13595
|
-
//
|
|
13596
|
-
// 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).
|
|
13597
13281
|
let typeGateBlocked = false;
|
|
13598
|
-
// Issue #684 regression guard: track which op the op-edit gate is
|
|
13599
|
-
// dry-running so an unrecognized server error (e.g. a plain
|
|
13600
|
-
// validation 400 like `"limit" must be 1000 or less`) can be
|
|
13601
|
-
// attributed to that specific operation in the catch-all below,
|
|
13602
|
-
// instead of being coarsened to the db-type level.
|
|
13603
|
-
let opGateContext = null;
|
|
13604
13282
|
// #2731 B4 — the concurrency guard is the LIVE timestamp the
|
|
13605
13283
|
// reconcile fetch just read (the gate carries it), not the
|
|
13606
13284
|
// manifest's, which can be arbitrarily stale and 409 on a write
|
|
@@ -13612,54 +13290,28 @@ What a push guarantees (two stages):
|
|
|
13612
13290
|
const validateExpectedModifiedAt = options.force
|
|
13613
13291
|
? undefined
|
|
13614
13292
|
: dbTypeExpectedModifiedAt;
|
|
13615
|
-
// Issue #1336 (codex round-1 P1/P2): the declared-access manifest
|
|
13616
|
-
// this push proposes (the parsed object, or null when the type
|
|
13617
|
-
// declares none / is clearing it). Forwarded as
|
|
13618
|
-
// `metadataManifestOverride` on the op dry-runs so op-access lints
|
|
13619
|
-
// run against the manifest the same push is landing, not the stale
|
|
13620
|
-
// stored one (existing type: a newly-declared key is honored; fresh
|
|
13621
|
-
// type: an undeclared ref is caught instead of skipped).
|
|
13622
|
-
const proposedManifest = typeConfig.metadataManifest ??
|
|
13623
|
-
null;
|
|
13624
13293
|
try {
|
|
13625
|
-
//
|
|
13626
|
-
//
|
|
13627
|
-
//
|
|
13628
|
-
//
|
|
13629
|
-
//
|
|
13630
|
-
// the server no longer has dry-runs its CREATE, not a PATCH.
|
|
13631
|
-
// Issue #1567: `computeTypeUpdateData` now always returns the
|
|
13632
|
-
// owned scalars, so gate on `typeLevelChange` (a genuine
|
|
13633
|
-
// declared/schema/clear change) rather than "payload
|
|
13634
|
-
// non-empty" — an operations-only edit must not run the
|
|
13635
|
-
// 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.
|
|
13636
13299
|
if (typeExistsOnServer && typeLevelChange) {
|
|
13637
|
-
|
|
13638
|
-
await client.updateDatabaseTypeConfig(resolvedAppId, dbType,
|
|
13639
|
-
// Forward the whole look-ahead (#813 / #1336 / #2732) so the
|
|
13640
|
-
// dry-run gates judge the post-push operation set — the same
|
|
13641
|
-
// payload the mutating PATCH below sends.
|
|
13642
|
-
{ ...validateUpdateData, ...pendingOpClaims }, validateExpectedModifiedAt, {
|
|
13300
|
+
await client.updateDatabaseTypeConfig(resolvedAppId, dbType, computeTypeUpdateData(), validateExpectedModifiedAt, {
|
|
13643
13301
|
dryRun: true,
|
|
13644
13302
|
acceptWarnings: !!options.acceptWarnings,
|
|
13645
13303
|
});
|
|
13646
13304
|
}
|
|
13647
13305
|
else if (!typeExistsOnServer) {
|
|
13648
13306
|
// Fresh type: run the config CREATE dry-run (issue #1336, codex
|
|
13649
|
-
// round-1 P2)
|
|
13650
|
-
//
|
|
13651
|
-
//
|
|
13652
|
-
// rule reported a clean `config push --dry-run` then failed only on
|
|
13653
|
-
// 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.
|
|
13654
13310
|
const createDryRunData = { databaseType: dbType };
|
|
13655
13311
|
if (typeConfig.ruleSetId)
|
|
13656
13312
|
createDryRunData.ruleSetId = typeConfig.ruleSetId;
|
|
13657
13313
|
if (typeConfig.triggers)
|
|
13658
13314
|
createDryRunData.triggers = typeConfig.triggers;
|
|
13659
|
-
if (typeConfig.metadataAccess)
|
|
13660
|
-
createDryRunData.metadataAccess = typeConfig.metadataAccess;
|
|
13661
|
-
if (typeConfig.defaultAccess)
|
|
13662
|
-
createDryRunData.defaultAccess = typeConfig.defaultAccess;
|
|
13663
13315
|
if (typeConfig.autoPopulatedFields)
|
|
13664
13316
|
createDryRunData.autoPopulatedFields =
|
|
13665
13317
|
typeConfig.autoPopulatedFields;
|
|
@@ -13682,175 +13334,12 @@ What a push guarantees (two stages):
|
|
|
13682
13334
|
throw createErr;
|
|
13683
13335
|
}
|
|
13684
13336
|
}
|
|
13685
|
-
// 2. Op-edit gate (op create/update dry-run) for every op in the
|
|
13686
|
-
// TOML. The server runs `runSchemaGate` and short-circuits
|
|
13687
|
-
// before persisting.
|
|
13688
|
-
//
|
|
13689
|
-
// The op gate is run against the schema THIS push is landing
|
|
13690
|
-
// (`schemaOverride`), not the stale stored schema — the real
|
|
13691
|
-
// apply path PATCHes the schema before creating/updating ops,
|
|
13692
|
-
// so an op that depends on a field this push adds must NOT be
|
|
13693
|
-
// falsely rejected (plan edge case: "op create that depends on
|
|
13694
|
-
// a schema landed earlier in the same push"). When the push
|
|
13695
|
-
// isn't changing the schema, leave the override unset so the
|
|
13696
|
-
// server uses the stored schema.
|
|
13697
|
-
//
|
|
13698
|
-
// Issue #915 (defect a): this gate now runs for FRESH types too
|
|
13699
|
-
// (previously guarded by `if (existingEntry)`). For a brand-new
|
|
13700
|
-
// db-type the server has no stored config, so we gate the op
|
|
13701
|
-
// against the PROPOSED local schema via `schemaOverride`. The
|
|
13702
|
-
// server's op-create dry-run accepts a missing type when a
|
|
13703
|
-
// `schemaOverride` is present (see
|
|
13704
|
-
// database-type-operations-controller.ts). A fresh schemaless
|
|
13705
|
-
// type (#666 bootstrap) sends an empty/absent override, and
|
|
13706
|
-
// `runSchemaGate` is a no-op without a schema — so that path
|
|
13707
|
-
// still succeeds.
|
|
13708
|
-
{
|
|
13709
|
-
const localHasSchema = typeof typeConfig.schema === "string" &&
|
|
13710
|
-
typeConfig.schema.trim().length > 0;
|
|
13711
|
-
let proposedSchema;
|
|
13712
|
-
if (typeExistsOnServer) {
|
|
13713
|
-
const validateUpdateData = computeTypeUpdateData();
|
|
13714
|
-
proposedSchema =
|
|
13715
|
-
"schema" in validateUpdateData
|
|
13716
|
-
? validateUpdateData.schema
|
|
13717
|
-
: undefined;
|
|
13718
|
-
}
|
|
13719
|
-
else {
|
|
13720
|
-
// Fresh type: the proposed schema is whatever the local TOML
|
|
13721
|
-
// declares. A schemaless fresh type (no `[models.*]`) sends an
|
|
13722
|
-
// empty-string override so the server takes the no-schema =
|
|
13723
|
-
// no-op gate path rather than falling back to a (nonexistent)
|
|
13724
|
-
// stored config — keeping the #666 bootstrap convergent.
|
|
13725
|
-
proposedSchema = localHasSchema ? typeConfig.schema : "";
|
|
13726
|
-
}
|
|
13727
|
-
for (const op of operations) {
|
|
13728
|
-
const existingOp = existingOpsForType[op.name];
|
|
13729
|
-
opGateContext = {
|
|
13730
|
-
action: existingOp ? "update" : "create",
|
|
13731
|
-
key: `${dbType}/${op.name}`,
|
|
13732
|
-
};
|
|
13733
|
-
if (existingOp) {
|
|
13734
|
-
await client.updateDatabaseTypeOperation(resolvedAppId, dbType, op.name, {
|
|
13735
|
-
type: op.type,
|
|
13736
|
-
modelName: op.modelName,
|
|
13737
|
-
access: op.access,
|
|
13738
|
-
definition: op.definition,
|
|
13739
|
-
params: op.params,
|
|
13740
|
-
}, options.force ? undefined : existingOp.modifiedAt, {
|
|
13741
|
-
dryRun: true,
|
|
13742
|
-
schemaOverride: proposedSchema,
|
|
13743
|
-
// Issue #1336 (P1/P2): gate the op-access lint against
|
|
13744
|
-
// the manifest THIS push is landing, not the stale
|
|
13745
|
-
// stored one.
|
|
13746
|
-
metadataManifestOverride: proposedManifest,
|
|
13747
|
-
});
|
|
13748
|
-
}
|
|
13749
|
-
else {
|
|
13750
|
-
await client.createDatabaseTypeOperation(resolvedAppId, dbType, {
|
|
13751
|
-
name: op.name,
|
|
13752
|
-
type: op.type,
|
|
13753
|
-
modelName: op.modelName,
|
|
13754
|
-
access: op.access,
|
|
13755
|
-
definition: op.definition,
|
|
13756
|
-
params: op.params,
|
|
13757
|
-
}, {
|
|
13758
|
-
dryRun: true,
|
|
13759
|
-
schemaOverride: proposedSchema,
|
|
13760
|
-
// Issue #1336 (P1/P2): as above — a fresh-type op dry-run
|
|
13761
|
-
// now lints against the proposed manifest instead of
|
|
13762
|
-
// skipping the lint entirely.
|
|
13763
|
-
metadataManifestOverride: proposedManifest,
|
|
13764
|
-
// Issue #915 (defect a, follow-up): for a fresh type the
|
|
13765
|
-
// server has no stored config to read `defaultAccess`
|
|
13766
|
-
// from, so an op that omits `access` to inherit the
|
|
13767
|
-
// TOML-declared type-level `defaultAccess` would be
|
|
13768
|
-
// falsely rejected. Thread the proposed value so the
|
|
13769
|
-
// fresh-type dry-run gates the same way the real push
|
|
13770
|
-
// (which lands the type first) will.
|
|
13771
|
-
defaultAccess: typeof typeConfig.defaultAccess === "string"
|
|
13772
|
-
? typeConfig.defaultAccess
|
|
13773
|
-
: undefined,
|
|
13774
|
-
});
|
|
13775
|
-
}
|
|
13776
|
-
}
|
|
13777
|
-
// Every op gate passed; clear the context so a later (non-op)
|
|
13778
|
-
// throw inside this try isn't misattributed to the last op.
|
|
13779
|
-
opGateContext = null;
|
|
13780
|
-
}
|
|
13781
13337
|
}
|
|
13782
13338
|
catch (err) {
|
|
13783
|
-
// A gate failure during validate aborts the whole type.
|
|
13784
|
-
//
|
|
13785
|
-
//
|
|
13786
|
-
|
|
13787
|
-
// the apply path's handling (validate-first doesn't change
|
|
13788
|
-
// conflict semantics).
|
|
13789
|
-
if (err instanceof SchemaBreaksOpsError) {
|
|
13790
|
-
schemaErrors.schemaBreaks.push({
|
|
13791
|
-
type: "database-type-config",
|
|
13792
|
-
key: dbType,
|
|
13793
|
-
operations: err.operations,
|
|
13794
|
-
message: err.message,
|
|
13795
|
-
});
|
|
13796
|
-
blockedDatabaseTypes.push({
|
|
13797
|
-
databaseType: dbType,
|
|
13798
|
-
reason: `schema change blocked: ${err.operations.length} kept operation(s) would break under the new schema`,
|
|
13799
|
-
});
|
|
13800
|
-
typeGateBlocked = true;
|
|
13801
|
-
}
|
|
13802
|
-
else if (err instanceof SchemaHasUncheckableOpsError) {
|
|
13803
|
-
schemaErrors.uncheckableOps.push({
|
|
13804
|
-
type: "database-type-config",
|
|
13805
|
-
key: dbType,
|
|
13806
|
-
operations: err.operations,
|
|
13807
|
-
message: err.message,
|
|
13808
|
-
});
|
|
13809
|
-
blockedDatabaseTypes.push({
|
|
13810
|
-
databaseType: dbType,
|
|
13811
|
-
reason: `schema change blocked: operation(s) have dynamic refs (re-run with --accept-warnings to commit)`,
|
|
13812
|
-
});
|
|
13813
|
-
typeGateBlocked = true;
|
|
13814
|
-
}
|
|
13815
|
-
else if (err instanceof OpsExistError) {
|
|
13816
|
-
schemaErrors.opsExist.push({
|
|
13817
|
-
type: "database-type-config",
|
|
13818
|
-
key: dbType,
|
|
13819
|
-
opCount: err.opCount,
|
|
13820
|
-
message: err.message,
|
|
13821
|
-
});
|
|
13822
|
-
blockedDatabaseTypes.push({
|
|
13823
|
-
databaseType: dbType,
|
|
13824
|
-
reason: `schema deletion blocked: ${err.opCount} operation(s) still registered after this push's deletes`,
|
|
13825
|
-
});
|
|
13826
|
-
typeGateBlocked = true;
|
|
13827
|
-
}
|
|
13828
|
-
else if (err instanceof SchemaRequiredError) {
|
|
13829
|
-
schemaErrors.schemaRequired.push({
|
|
13830
|
-
type: "operation",
|
|
13831
|
-
key: `${dbType}/_`,
|
|
13832
|
-
message: err.message,
|
|
13833
|
-
});
|
|
13834
|
-
blockedDatabaseTypes.push({
|
|
13835
|
-
databaseType: dbType,
|
|
13836
|
-
reason: `operation push blocked: type has no schema set`,
|
|
13837
|
-
});
|
|
13838
|
-
typeGateBlocked = true;
|
|
13839
|
-
}
|
|
13840
|
-
else if (err instanceof OperationRefError) {
|
|
13841
|
-
schemaErrors.opRefs.push({
|
|
13842
|
-
type: "operation",
|
|
13843
|
-
key: dbType,
|
|
13844
|
-
refs: err.refs,
|
|
13845
|
-
message: err.message,
|
|
13846
|
-
});
|
|
13847
|
-
blockedDatabaseTypes.push({
|
|
13848
|
-
databaseType: dbType,
|
|
13849
|
-
reason: `operation push blocked: unresolved references in the schema`,
|
|
13850
|
-
});
|
|
13851
|
-
typeGateBlocked = true;
|
|
13852
|
-
}
|
|
13853
|
-
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) {
|
|
13854
13343
|
// Surface the conflict and skip this type (don't blow away the
|
|
13855
13344
|
// server with mutating calls). Mirrors the apply path.
|
|
13856
13345
|
conflicts.push({
|
|
@@ -13871,14 +13360,6 @@ What a push guarantees (two stages):
|
|
|
13871
13360
|
});
|
|
13872
13361
|
typeGateBlocked = true;
|
|
13873
13362
|
}
|
|
13874
|
-
else if (opGateContext) {
|
|
13875
|
-
// The op-edit gate dry-run threw a plain (non-gate) error —
|
|
13876
|
-
// e.g. an invalid op `definition` the server rejects with a
|
|
13877
|
-
// 400. The validate-first loop already knows which op failed,
|
|
13878
|
-
// so name it (issue #684) instead of coarsening to the
|
|
13879
|
-
// db-type level. Mirrors the apply path's per-op wraps.
|
|
13880
|
-
throw wrapEntityError(err, opGateContext.action, "operation", opGateContext.key);
|
|
13881
|
-
}
|
|
13882
13363
|
else {
|
|
13883
13364
|
throw wrapEntityError(err, "update", "database type", dbType);
|
|
13884
13365
|
}
|
|
@@ -13898,17 +13379,6 @@ What a push guarantees (two stages):
|
|
|
13898
13379
|
else {
|
|
13899
13380
|
changes.push({ type: "database-type-config", action: "create", key: dbType });
|
|
13900
13381
|
}
|
|
13901
|
-
for (const op of operations) {
|
|
13902
|
-
const existingOp = existingOpsForType[op.name];
|
|
13903
|
-
changes.push({
|
|
13904
|
-
type: "operation",
|
|
13905
|
-
action: existingOp ? "update" : "create",
|
|
13906
|
-
key: `${dbType}/${op.name}`,
|
|
13907
|
-
});
|
|
13908
|
-
}
|
|
13909
|
-
for (const delName of pendingOpDeletes) {
|
|
13910
|
-
changes.push({ type: "operation", action: "delete", key: `${dbType}/${delName}` });
|
|
13911
|
-
}
|
|
13912
13382
|
continue;
|
|
13913
13383
|
}
|
|
13914
13384
|
// A live type the manifest never recorded is about to be updated,
|
|
@@ -13934,9 +13404,8 @@ What a push guarantees (two stages):
|
|
|
13934
13404
|
// never recorded is updated rather than re-created.
|
|
13935
13405
|
if (typeExistsOnServer) {
|
|
13936
13406
|
// Update existing type config — only when there is a genuine
|
|
13937
|
-
// type-level change.
|
|
13938
|
-
//
|
|
13939
|
-
// 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)
|
|
13940
13409
|
// and a spurious "Updated database type". Issue #1567: the owned
|
|
13941
13410
|
// scalars are always in `updateData` now (value-or-`null`), so the
|
|
13942
13411
|
// skip decision moved from "payload non-empty" to the
|
|
@@ -13953,18 +13422,7 @@ What a push guarantees (two stages):
|
|
|
13953
13422
|
// `schema: null` clears it server-side; codex review gap on
|
|
13954
13423
|
// PR #766). Built by the shared helper so the validate pass
|
|
13955
13424
|
// and apply pass agree (issue #813).
|
|
13956
|
-
|
|
13957
|
-
// went on the dry-run above — the gates it feeds run on this
|
|
13958
|
-
// call, and the operation creates/updates/deletes it
|
|
13959
|
-
// describes still happen in step 3 below. It is sent whole
|
|
13960
|
-
// and unconditionally: a push that only rewrites operations
|
|
13961
|
-
// has no deletes to report, and gating on that was what kept
|
|
13962
|
-
// the #2732 case (model removal + operation rewrites) from
|
|
13963
|
-
// ever reaching the server as one push.
|
|
13964
|
-
const updateData = {
|
|
13965
|
-
...computeTypeUpdateData(),
|
|
13966
|
-
...pendingOpClaims,
|
|
13967
|
-
};
|
|
13425
|
+
const updateData = computeTypeUpdateData();
|
|
13968
13426
|
if (typeLevelChange) {
|
|
13969
13427
|
changes.push({ type: "database-type-config", action: "update", key: dbType });
|
|
13970
13428
|
const updated = await client.updateDatabaseTypeConfig(resolvedAppId, dbType, updateData, expectedModifiedAt, {
|
|
@@ -13973,8 +13431,8 @@ What a push guarantees (two stages):
|
|
|
13973
13431
|
});
|
|
13974
13432
|
info(` Updated database type: ${dbType}`);
|
|
13975
13433
|
// Issue #1567: name each owned scalar this push cleared, so
|
|
13976
|
-
// removing
|
|
13977
|
-
//
|
|
13434
|
+
// removing a rule (triggers, autoPopulatedFields) is never
|
|
13435
|
+
// silent.
|
|
13978
13436
|
reportClearedFields("Cleared ");
|
|
13979
13437
|
printStaleInstanceWarning(updated);
|
|
13980
13438
|
if (syncState?.entities?.databaseTypes?.[dbType] && updated?.modifiedAt) {
|
|
@@ -13991,28 +13449,11 @@ What a push guarantees (two stages):
|
|
|
13991
13449
|
if (err instanceof ConflictError) {
|
|
13992
13450
|
// #2731 B5 — the server moved between the reconcile fetch
|
|
13993
13451
|
// and this write, so recompute the field diff against the
|
|
13994
|
-
// record that is there NOW
|
|
13995
|
-
// subscriptions, which the comparison treats as one unit).
|
|
13452
|
+
// record that is there NOW.
|
|
13996
13453
|
conflicts.push(await lateConflict("database-type-config", dbType, err, expectedModifiedAt, refetchLiveDatabaseType, databaseTypeSpec, tomlData));
|
|
13997
13454
|
// The PATCH was rejected: this file did not land.
|
|
13998
13455
|
fileHadConflict = true;
|
|
13999
13456
|
}
|
|
14000
|
-
else if (err instanceof SchemaBreaksOpsError) {
|
|
14001
|
-
schemaErrors.schemaBreaks.push({
|
|
14002
|
-
type: "database-type-config",
|
|
14003
|
-
key: dbType,
|
|
14004
|
-
operations: err.operations,
|
|
14005
|
-
message: err.message,
|
|
14006
|
-
});
|
|
14007
|
-
}
|
|
14008
|
-
else if (err instanceof SchemaHasUncheckableOpsError) {
|
|
14009
|
-
schemaErrors.uncheckableOps.push({
|
|
14010
|
-
type: "database-type-config",
|
|
14011
|
-
key: dbType,
|
|
14012
|
-
operations: err.operations,
|
|
14013
|
-
message: err.message,
|
|
14014
|
-
});
|
|
14015
|
-
}
|
|
14016
13457
|
else if (err instanceof TomlParseError) {
|
|
14017
13458
|
schemaErrors.tomlParse.push({
|
|
14018
13459
|
type: "database-type-config",
|
|
@@ -14022,14 +13463,6 @@ What a push guarantees (two stages):
|
|
|
14022
13463
|
message: err.message,
|
|
14023
13464
|
});
|
|
14024
13465
|
}
|
|
14025
|
-
else if (err instanceof OpsExistError) {
|
|
14026
|
-
schemaErrors.opsExist.push({
|
|
14027
|
-
type: "database-type-config",
|
|
14028
|
-
key: dbType,
|
|
14029
|
-
opCount: err.opCount,
|
|
14030
|
-
message: err.message,
|
|
14031
|
-
});
|
|
14032
|
-
}
|
|
14033
13466
|
else {
|
|
14034
13467
|
throw wrapEntityError(err, "update", "database type", dbType);
|
|
14035
13468
|
}
|
|
@@ -14056,10 +13489,6 @@ What a push guarantees (two stages):
|
|
|
14056
13489
|
createData.ruleSetId = typeConfig.ruleSetId;
|
|
14057
13490
|
if (typeConfig.triggers)
|
|
14058
13491
|
createData.triggers = typeConfig.triggers;
|
|
14059
|
-
if (typeConfig.metadataAccess)
|
|
14060
|
-
createData.metadataAccess = typeConfig.metadataAccess;
|
|
14061
|
-
if (typeConfig.defaultAccess)
|
|
14062
|
-
createData.defaultAccess = typeConfig.defaultAccess;
|
|
14063
13492
|
if (typeConfig.autoPopulatedFields)
|
|
14064
13493
|
createData.autoPopulatedFields = typeConfig.autoPopulatedFields;
|
|
14065
13494
|
if (typeConfig.timestamps)
|
|
@@ -14127,11 +13556,7 @@ What a push guarantees (two stages):
|
|
|
14127
13556
|
// remote config and refuse to mutate it without a baseline:
|
|
14128
13557
|
// surface it as a SKIPPED type (reusing the #813 blocked-type
|
|
14129
13558
|
// disclosure + non-zero exit) so the user runs `config pull`
|
|
14130
|
-
// first.
|
|
14131
|
-
// operations are created against the un-reconciled type.
|
|
14132
|
-
const existingOpsRemote = await client
|
|
14133
|
-
.listDatabaseTypeOperations(resolvedAppId, dbType)
|
|
14134
|
-
.catch(() => []);
|
|
13559
|
+
// first.
|
|
14135
13560
|
const hasNonDefaultField = (v) => {
|
|
14136
13561
|
if (v === null || v === undefined)
|
|
14137
13562
|
return false;
|
|
@@ -14141,12 +13566,8 @@ What a push guarantees (two stages):
|
|
|
14141
13566
|
return Object.keys(v).length > 0;
|
|
14142
13567
|
return true;
|
|
14143
13568
|
};
|
|
14144
|
-
const isMinimalAutoCreated =
|
|
14145
|
-
existingOpsRemote.length === 0) &&
|
|
14146
|
-
!hasNonDefaultField(existing?.ruleSetId) &&
|
|
13569
|
+
const isMinimalAutoCreated = !hasNonDefaultField(existing?.ruleSetId) &&
|
|
14147
13570
|
!hasNonDefaultField(existing?.triggers) &&
|
|
14148
|
-
!hasNonDefaultField(existing?.metadataAccess) &&
|
|
14149
|
-
!hasNonDefaultField(existing?.defaultAccess) &&
|
|
14150
13571
|
!hasNonDefaultField(existing?.autoPopulatedFields) &&
|
|
14151
13572
|
!hasNonDefaultField(existing?.timestamps) &&
|
|
14152
13573
|
!hasNonDefaultField(existing?.schema);
|
|
@@ -14180,13 +13601,6 @@ What a push guarantees (two stages):
|
|
|
14180
13601
|
action: "update",
|
|
14181
13602
|
key: dbType,
|
|
14182
13603
|
});
|
|
14183
|
-
for (const op of operations) {
|
|
14184
|
-
changes.push({
|
|
14185
|
-
type: "operation",
|
|
14186
|
-
action: "create",
|
|
14187
|
-
key: `${dbType}/${op.name}`,
|
|
14188
|
-
});
|
|
14189
|
-
}
|
|
14190
13604
|
continue;
|
|
14191
13605
|
}
|
|
14192
13606
|
info(` reconciled auto-created database type \`${dbType}\``);
|
|
@@ -14211,7 +13625,7 @@ What a push guarantees (two stages):
|
|
|
14211
13625
|
// "modified since last sync" CONFLICT (Q4). Subsequent
|
|
14212
13626
|
// pushes carry the baseline just seeded above. Skip the
|
|
14213
13627
|
// PATCH entirely when the TOML declares no type-level
|
|
14214
|
-
// fields
|
|
13628
|
+
// fields so we don't send an empty
|
|
14215
13629
|
// body, which the server rejects with HTTP 400 (#369).
|
|
14216
13630
|
const reconcileData = computeTypeUpdateData();
|
|
14217
13631
|
if (Object.keys(reconcileData).length > 0) {
|
|
@@ -14230,11 +13644,6 @@ What a push guarantees (two stages):
|
|
|
14230
13644
|
: syncState.entities.databaseTypes[dbType].hasSchema;
|
|
14231
13645
|
}
|
|
14232
13646
|
}
|
|
14233
|
-
// Operations declared in the TOML flow through the existing
|
|
14234
|
-
// op sub-loop below. `existingOps` is derived from
|
|
14235
|
-
// `existingEntry` (the pre-push sync-state snapshot, which is
|
|
14236
|
-
// empty here), so every op is CREATEd — matching the empty
|
|
14237
|
-
// auto-created type on the server.
|
|
14238
13647
|
}
|
|
14239
13648
|
else {
|
|
14240
13649
|
throw wrapEntityError(err, "create", "database type", dbType);
|
|
@@ -14242,315 +13651,22 @@ What a push guarantees (two stages):
|
|
|
14242
13651
|
}
|
|
14243
13652
|
}
|
|
14244
13653
|
}
|
|
14245
|
-
//
|
|
14246
|
-
// the
|
|
14247
|
-
//
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
const existingOp = existingOps[op.name];
|
|
14252
|
-
if (existingOp) {
|
|
14253
|
-
// Update existing operation
|
|
14254
|
-
changes.push({ type: "operation", action: "update", key: `${dbType}/${op.name}` });
|
|
14255
|
-
if (!options.dryRun) {
|
|
14256
|
-
// #2731 B4 — the LIVE row's timestamp when the reconcile read
|
|
14257
|
-
// it, the manifest's otherwise (degraded path).
|
|
14258
|
-
const expectedOpModifiedAt = options.force
|
|
14259
|
-
? undefined
|
|
14260
|
-
: liveOpsByName?.get(op.name)?.modifiedAt ??
|
|
14261
|
-
existingOp.modifiedAt;
|
|
14262
|
-
try {
|
|
14263
|
-
const updated = await client.updateDatabaseTypeOperation(resolvedAppId, dbType, op.name, {
|
|
14264
|
-
// Include `type` so the server can detect/apply
|
|
14265
|
-
// type-in-place changes (issue #692). When the type
|
|
14266
|
-
// matches the stored value, this is a no-op.
|
|
14267
|
-
type: op.type,
|
|
14268
|
-
modelName: op.modelName,
|
|
14269
|
-
access: op.access,
|
|
14270
|
-
definition: op.definition,
|
|
14271
|
-
params: op.params,
|
|
14272
|
-
}, expectedOpModifiedAt);
|
|
14273
|
-
info(` Updated operation: ${dbType}/${op.name}`);
|
|
14274
|
-
// Recorded even when the manifest had never seen this row:
|
|
14275
|
-
// the update targeted a row live state named, and a state
|
|
14276
|
-
// that stays silent about it would send this same push
|
|
14277
|
-
// again on the next run.
|
|
14278
|
-
const opsEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
14279
|
-
if (opsEntry && updated?.modifiedAt) {
|
|
14280
|
-
if (!opsEntry.operations)
|
|
14281
|
-
opsEntry.operations = {};
|
|
14282
|
-
opsEntry.operations[op.name] = { modifiedAt: updated.modifiedAt };
|
|
14283
|
-
}
|
|
14284
|
-
}
|
|
14285
|
-
catch (err) {
|
|
14286
|
-
if (err instanceof ConflictError) {
|
|
14287
|
-
// #2731 B5 — an operation is part of this file's compared
|
|
14288
|
-
// surface, so its 409 is reported like the type's own:
|
|
14289
|
-
// against the record that is there NOW, with the field
|
|
14290
|
-
// diff (`operations` as a unit) rather than two
|
|
14291
|
-
// timestamps the operator cannot act on.
|
|
14292
|
-
conflicts.push({
|
|
14293
|
-
...(await lateConflict("operation", `${dbType}/${op.name}`, err, expectedOpModifiedAt, refetchLiveDatabaseType, databaseTypeSpec, tomlData)),
|
|
14294
|
-
});
|
|
14295
|
-
// The op was rejected, so this file did not fully land.
|
|
14296
|
-
fileHadConflict = true;
|
|
14297
|
-
}
|
|
14298
|
-
else if (err instanceof SchemaRequiredError) {
|
|
14299
|
-
schemaErrors.schemaRequired.push({
|
|
14300
|
-
type: "operation",
|
|
14301
|
-
key: `${dbType}/${op.name}`,
|
|
14302
|
-
message: err.message,
|
|
14303
|
-
});
|
|
14304
|
-
// Issue #915 (defect b): a gate-rejected op means this
|
|
14305
|
-
// file's state did NOT fully land — don't poison the
|
|
14306
|
-
// content hash (see the contentHash write below).
|
|
14307
|
-
fileHadGateRejectedOp = true;
|
|
14308
|
-
}
|
|
14309
|
-
else if (err instanceof OperationRefError) {
|
|
14310
|
-
schemaErrors.opRefs.push({
|
|
14311
|
-
type: "operation",
|
|
14312
|
-
key: `${dbType}/${op.name}`,
|
|
14313
|
-
refs: err.refs,
|
|
14314
|
-
message: err.message,
|
|
14315
|
-
});
|
|
14316
|
-
fileHadGateRejectedOp = true;
|
|
14317
|
-
}
|
|
14318
|
-
else {
|
|
14319
|
-
throw wrapEntityError(err, "update", "operation", `${dbType}/${op.name}`);
|
|
14320
|
-
}
|
|
14321
|
-
}
|
|
14322
|
-
}
|
|
14323
|
-
}
|
|
14324
|
-
else {
|
|
14325
|
-
// Create new operation
|
|
14326
|
-
changes.push({ type: "operation", action: "create", key: `${dbType}/${op.name}` });
|
|
14327
|
-
if (!options.dryRun) {
|
|
14328
|
-
try {
|
|
14329
|
-
const created = await client.createDatabaseTypeOperation(resolvedAppId, dbType, {
|
|
14330
|
-
name: op.name,
|
|
14331
|
-
type: op.type,
|
|
14332
|
-
modelName: op.modelName,
|
|
14333
|
-
access: op.access,
|
|
14334
|
-
definition: op.definition,
|
|
14335
|
-
params: op.params,
|
|
14336
|
-
});
|
|
14337
|
-
info(` Created operation: ${dbType}/${op.name}`);
|
|
14338
|
-
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
14339
|
-
if (!syncState.entities.databaseTypes[dbType].operations) {
|
|
14340
|
-
syncState.entities.databaseTypes[dbType].operations = {};
|
|
14341
|
-
}
|
|
14342
|
-
syncState.entities.databaseTypes[dbType].operations[op.name] = {
|
|
14343
|
-
modifiedAt: created?.modifiedAt || new Date().toISOString(),
|
|
14344
|
-
};
|
|
14345
|
-
}
|
|
14346
|
-
}
|
|
14347
|
-
catch (err) {
|
|
14348
|
-
if (err instanceof SchemaRequiredError) {
|
|
14349
|
-
schemaErrors.schemaRequired.push({
|
|
14350
|
-
type: "operation",
|
|
14351
|
-
key: `${dbType}/${op.name}`,
|
|
14352
|
-
message: err.message,
|
|
14353
|
-
});
|
|
14354
|
-
// Issue #915 (defect b): a gate-rejected op means this
|
|
14355
|
-
// file's state did NOT fully land — don't poison the
|
|
14356
|
-
// content hash (see the contentHash write below).
|
|
14357
|
-
fileHadGateRejectedOp = true;
|
|
14358
|
-
}
|
|
14359
|
-
else if (err instanceof OperationRefError) {
|
|
14360
|
-
schemaErrors.opRefs.push({
|
|
14361
|
-
type: "operation",
|
|
14362
|
-
key: `${dbType}/${op.name}`,
|
|
14363
|
-
refs: err.refs,
|
|
14364
|
-
message: err.message,
|
|
14365
|
-
});
|
|
14366
|
-
fileHadGateRejectedOp = true;
|
|
14367
|
-
}
|
|
14368
|
-
else {
|
|
14369
|
-
throw wrapEntityError(err, "create", "operation", `${dbType}/${op.name}`);
|
|
14370
|
-
}
|
|
14371
|
-
}
|
|
14372
|
-
}
|
|
14373
|
-
}
|
|
14374
|
-
}
|
|
14375
|
-
// Delete operations that were removed from TOML
|
|
14376
|
-
for (const existingOpName of Object.keys(existingOps)) {
|
|
14377
|
-
if (!tomlOpNames.has(existingOpName)) {
|
|
14378
|
-
changes.push({ type: "operation", action: "delete", key: `${dbType}/${existingOpName}` });
|
|
14379
|
-
if (!options.dryRun) {
|
|
14380
|
-
try {
|
|
14381
|
-
await client.deleteDatabaseTypeOperation(resolvedAppId, dbType, existingOpName);
|
|
14382
|
-
info(` Deleted operation: ${dbType}/${existingOpName}`);
|
|
14383
|
-
if (syncState?.entities?.databaseTypes?.[dbType]?.operations) {
|
|
14384
|
-
delete syncState.entities.databaseTypes[dbType].operations[existingOpName];
|
|
14385
|
-
}
|
|
14386
|
-
}
|
|
14387
|
-
catch (err) {
|
|
14388
|
-
throw wrapEntityError(err, "delete", "operation", `${dbType}/${existingOpName}`);
|
|
14389
|
-
}
|
|
14390
|
-
}
|
|
14391
|
-
}
|
|
14392
|
-
}
|
|
14393
|
-
// Process subscriptions for this type (issue #803). Mirrors the
|
|
14394
|
-
// operations reconcile above, keyed on `subscriptionKey`:
|
|
14395
|
-
// create new keys, PUT changed/existing keys, delete keys present
|
|
14396
|
-
// in SyncState but absent from the TOML. Server errors (409 key
|
|
14397
|
-
// collision, `#`-in-key 400, max-20 400, missing-required 400)
|
|
14398
|
-
// are surfaced cleanly via `wrapEntityError` — named, not a crash.
|
|
14399
|
-
const existingSubs = liveChildBaseline(liveSubsByName, syncState?.entities?.databaseTypes?.[dbType]?.subscriptions) ??
|
|
14400
|
-
((typeExistsOnServer
|
|
14401
|
-
? syncState?.entities?.databaseTypes?.[dbType]?.subscriptions
|
|
14402
|
-
: undefined) || {});
|
|
14403
|
-
const tomlSubKeys = new Set(subscriptions.map((sub) => sub.subscriptionKey));
|
|
14404
|
-
for (const sub of subscriptions) {
|
|
14405
|
-
const subKey = sub.subscriptionKey;
|
|
14406
|
-
const existingSub = existingSubs[subKey];
|
|
14407
|
-
if (existingSub) {
|
|
14408
|
-
// Update existing subscription (PUT — idempotent).
|
|
14409
|
-
changes.push({ type: "subscription", action: "update", key: `${dbType}/${subKey}` });
|
|
14410
|
-
if (!options.dryRun) {
|
|
14411
|
-
// #2731 B4 — the LIVE row's timestamp when the reconcile read
|
|
14412
|
-
// it, the manifest's otherwise (degraded path). Subscriptions
|
|
14413
|
-
// are part of a converted type's push units, so a concurrent
|
|
14414
|
-
// server edit has to be a 409 here too, not a silent
|
|
14415
|
-
// overwrite.
|
|
14416
|
-
const expectedSubModifiedAt = options.force
|
|
14417
|
-
? undefined
|
|
14418
|
-
: liveSubsByName?.get(subKey)?.modifiedAt ??
|
|
14419
|
-
existingSub.modifiedAt;
|
|
14420
|
-
try {
|
|
14421
|
-
const updated = await client.updateDatabaseTypeSubscription(resolvedAppId, dbType, subKey, {
|
|
14422
|
-
displayName: sub.displayName,
|
|
14423
|
-
modelName: sub.modelName,
|
|
14424
|
-
filter: sub.filter,
|
|
14425
|
-
access: sub.access,
|
|
14426
|
-
description: sub.description,
|
|
14427
|
-
select: sub.select,
|
|
14428
|
-
emit: sub.emit,
|
|
14429
|
-
params: sub.params,
|
|
14430
|
-
status: sub.status,
|
|
14431
|
-
}, expectedSubModifiedAt);
|
|
14432
|
-
info(` Updated subscription: ${dbType}/${subKey}`);
|
|
14433
|
-
// Gate the SyncState write on a server-returned
|
|
14434
|
-
// `modifiedAt`, mirroring the operations precedent above
|
|
14435
|
-
// (don't fall back to a fresh local timestamp) — and record
|
|
14436
|
-
// a row the manifest had never seen, for the same reason.
|
|
14437
|
-
const subsEntry = syncState?.entities?.databaseTypes?.[dbType];
|
|
14438
|
-
if (subsEntry && updated?.modifiedAt) {
|
|
14439
|
-
if (!subsEntry.subscriptions)
|
|
14440
|
-
subsEntry.subscriptions = {};
|
|
14441
|
-
subsEntry.subscriptions[subKey] = { modifiedAt: updated.modifiedAt };
|
|
14442
|
-
}
|
|
14443
|
-
}
|
|
14444
|
-
catch (err) {
|
|
14445
|
-
if (err instanceof ConflictError) {
|
|
14446
|
-
conflicts.push({
|
|
14447
|
-
...(await lateConflict("subscription", `${dbType}/${subKey}`, err, expectedSubModifiedAt, refetchLiveDatabaseType, databaseTypeSpec, tomlData)),
|
|
14448
|
-
});
|
|
14449
|
-
// The subscription was rejected, so this file did not
|
|
14450
|
-
// fully land — the baseline stamp below must not claim it
|
|
14451
|
-
// did.
|
|
14452
|
-
fileHadConflict = true;
|
|
14453
|
-
}
|
|
14454
|
-
else {
|
|
14455
|
-
throw wrapEntityError(err, "update", "subscription", `${dbType}/${subKey}`);
|
|
14456
|
-
}
|
|
14457
|
-
}
|
|
14458
|
-
}
|
|
14459
|
-
}
|
|
14460
|
-
else {
|
|
14461
|
-
// Create new subscription (POST).
|
|
14462
|
-
changes.push({ type: "subscription", action: "create", key: `${dbType}/${subKey}` });
|
|
14463
|
-
if (!options.dryRun) {
|
|
14464
|
-
try {
|
|
14465
|
-
const created = await client.createDatabaseTypeSubscription(resolvedAppId, dbType, {
|
|
14466
|
-
subscriptionKey: subKey,
|
|
14467
|
-
displayName: sub.displayName,
|
|
14468
|
-
modelName: sub.modelName,
|
|
14469
|
-
filter: sub.filter,
|
|
14470
|
-
access: sub.access,
|
|
14471
|
-
description: sub.description,
|
|
14472
|
-
select: sub.select,
|
|
14473
|
-
emit: sub.emit,
|
|
14474
|
-
params: sub.params,
|
|
14475
|
-
status: sub.status,
|
|
14476
|
-
});
|
|
14477
|
-
info(` Created subscription: ${dbType}/${subKey}`);
|
|
14478
|
-
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
14479
|
-
if (!syncState.entities.databaseTypes[dbType].subscriptions) {
|
|
14480
|
-
syncState.entities.databaseTypes[dbType].subscriptions = {};
|
|
14481
|
-
}
|
|
14482
|
-
syncState.entities.databaseTypes[dbType].subscriptions[subKey] = {
|
|
14483
|
-
modifiedAt: created?.modifiedAt || new Date().toISOString(),
|
|
14484
|
-
};
|
|
14485
|
-
}
|
|
14486
|
-
}
|
|
14487
|
-
catch (err) {
|
|
14488
|
-
throw wrapEntityError(err, "create", "subscription", `${dbType}/${subKey}`);
|
|
14489
|
-
}
|
|
14490
|
-
}
|
|
14491
|
-
}
|
|
14492
|
-
}
|
|
14493
|
-
// Delete subscriptions that were removed from TOML (hard-delete on
|
|
14494
|
-
// current `main`, PR #787 — frees the key for reuse). A 409 on
|
|
14495
|
-
// recreate is impossible here since we delete first; the pull-side
|
|
14496
|
-
// "active only" filter (controller `list` excludes archived)
|
|
14497
|
-
// agrees because hard-delete leaves no archived row behind.
|
|
14498
|
-
for (const existingSubKey of Object.keys(existingSubs)) {
|
|
14499
|
-
if (!tomlSubKeys.has(existingSubKey)) {
|
|
14500
|
-
changes.push({ type: "subscription", action: "delete", key: `${dbType}/${existingSubKey}` });
|
|
14501
|
-
if (!options.dryRun) {
|
|
14502
|
-
try {
|
|
14503
|
-
await client.deleteDatabaseTypeSubscription(resolvedAppId, dbType, existingSubKey);
|
|
14504
|
-
info(` Deleted subscription: ${dbType}/${existingSubKey}`);
|
|
14505
|
-
if (syncState?.entities?.databaseTypes?.[dbType]?.subscriptions) {
|
|
14506
|
-
delete syncState.entities.databaseTypes[dbType].subscriptions[existingSubKey];
|
|
14507
|
-
}
|
|
14508
|
-
}
|
|
14509
|
-
catch (err) {
|
|
14510
|
-
throw wrapEntityError(err, "delete", "subscription", `${dbType}/${existingSubKey}`);
|
|
14511
|
-
}
|
|
14512
|
-
}
|
|
14513
|
-
}
|
|
14514
|
-
}
|
|
14515
|
-
// Refresh the file's content hash after processing operations +
|
|
14516
|
-
// subscriptions (issue #803, behavior g). The type-config update
|
|
14517
|
-
// block only refreshes the hash when a type-level field changed
|
|
14518
|
-
// (its `updateData` was non-empty); an operations- or
|
|
14519
|
-
// subscriptions-only edit left a stale hash, so the next push
|
|
14520
|
-
// would re-PUT every op/sub instead of skipping the unchanged
|
|
14521
|
-
// file. Recompute here so a truly-unchanged subsequent push skips
|
|
14522
|
-
// the whole file via `shouldPushFile`. `--force` bypasses that
|
|
14523
|
-
// skip, so forced re-pushes are unaffected.
|
|
14524
|
-
//
|
|
14525
|
-
// Issue #915 (defect b): when the gate rejected an op for this
|
|
14526
|
-
// file, do NOT record a matching content hash. The db-type itself
|
|
14527
|
-
// may have been created (the push is non-transactional), but the
|
|
14528
|
-
// blocked op never landed — a matching hash would make
|
|
14529
|
-
// `shouldPushFile` short-circuit the next push and hide the missing
|
|
14530
|
-
// op as "in sync". Clear the hash instead of writing it: an empty
|
|
14531
|
-
// hash forces `shouldPushFile` to re-evaluate the file, so a
|
|
14532
|
-
// subsequent `config push --dry-run` still surfaces the op as drift
|
|
14533
|
-
// and a corrected re-push converges without manual file edits. We
|
|
14534
|
-
// clear (rather than merely skip) so the fresh-type create path's
|
|
14535
|
-
// earlier optimistic hash write (see `createDatabaseTypeConfig`
|
|
14536
|
-
// above) is also invalidated.
|
|
14537
|
-
//
|
|
14538
|
-
// #2731 B4: the semantic baseline is refreshed here too, and
|
|
14539
|
-
// cleared alongside the byte hash when an op was gate-rejected —
|
|
14540
|
-
// a baseline claiming convergence would make the next run read the
|
|
14541
|
-
// missing op as server drift rather than as work still to do. A
|
|
14542
|
-
// 409 on the type or one of its ops means the same thing: the
|
|
14543
|
-
// 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.
|
|
14544
13660
|
if (!options.dryRun && syncState?.entities?.databaseTypes?.[dbType]) {
|
|
14545
|
-
if (
|
|
13661
|
+
if (fileHadConflict) {
|
|
14546
13662
|
syncState.entities.databaseTypes[dbType].contentHash = "";
|
|
14547
13663
|
syncState.entities.databaseTypes[dbType].semanticHash = undefined;
|
|
14548
13664
|
}
|
|
14549
13665
|
else {
|
|
14550
13666
|
syncState.entities.databaseTypes[dbType].contentHash =
|
|
14551
13667
|
computeFileHash(filePath);
|
|
14552
|
-
// B4 — the baseline reads back what the server now holds
|
|
14553
|
-
//
|
|
13668
|
+
// B4 — the baseline reads back what the server now holds.
|
|
13669
|
+
// Taking it from the local file would
|
|
14554
13670
|
// stamp a value the handlers normalized as the state both sides
|
|
14555
13671
|
// agreed on, and the next local edit would report a conflict
|
|
14556
13672
|
// against a difference this push itself created. A read that
|
|
@@ -14582,6 +13698,9 @@ What a push guarantees (two stages):
|
|
|
14582
13698
|
// import that escapes the config tree aborts this function before
|
|
14583
13699
|
// anything is sent for it (behavior 12).
|
|
14584
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();
|
|
14585
13704
|
if (existsSync(functionsPushDir)) {
|
|
14586
13705
|
// #3182 — the typed handle. Written BEFORE the bundles are built, so
|
|
14587
13706
|
// an author who runs `config push` after editing a schema gets the
|
|
@@ -14676,6 +13795,15 @@ What a push guarantees (two stages):
|
|
|
14676
13795
|
withDerivedIdentity(tomlData, "function", { key });
|
|
14677
13796
|
const existingEntry = syncState?.entities?.functions?.[key];
|
|
14678
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
|
+
};
|
|
14679
13807
|
// Build first: nothing is sent for a function whose code does not
|
|
14680
13808
|
// compile or whose imports leave the tree.
|
|
14681
13809
|
let push;
|
|
@@ -14695,6 +13823,7 @@ What a push guarantees (two stages):
|
|
|
14695
13823
|
key,
|
|
14696
13824
|
message: err.message,
|
|
14697
13825
|
});
|
|
13826
|
+
markUnapplied();
|
|
14698
13827
|
continue;
|
|
14699
13828
|
}
|
|
14700
13829
|
// #3455 (D3455-004) — the project declares a document schema this
|
|
@@ -14721,6 +13850,7 @@ What a push guarantees (two stages):
|
|
|
14721
13850
|
key,
|
|
14722
13851
|
message: failure.message,
|
|
14723
13852
|
});
|
|
13853
|
+
markUnapplied();
|
|
14724
13854
|
continue;
|
|
14725
13855
|
}
|
|
14726
13856
|
}
|
|
@@ -14838,6 +13968,7 @@ What a push guarantees (two stages):
|
|
|
14838
13968
|
// the same contract `applyFailures` gives every other type; a throw
|
|
14839
13969
|
// here would let one refusal block a whole repository.
|
|
14840
13970
|
let functionId = liveFunction?.functionId ?? existingEntry?.id;
|
|
13971
|
+
let createdThisPush = false;
|
|
14841
13972
|
let pushedVersion;
|
|
14842
13973
|
try {
|
|
14843
13974
|
if (!functionId) {
|
|
@@ -14862,6 +13993,7 @@ What a push guarantees (two stages):
|
|
|
14862
13993
|
return existing;
|
|
14863
13994
|
});
|
|
14864
13995
|
functionId = created.functionId;
|
|
13996
|
+
createdThisPush = true;
|
|
14865
13997
|
info(` Created function: ${key}`);
|
|
14866
13998
|
}
|
|
14867
13999
|
else if (headerAction !== "skip") {
|
|
@@ -14884,6 +14016,11 @@ What a push guarantees (two stages):
|
|
|
14884
14016
|
key,
|
|
14885
14017
|
message: String(err?.message ?? err),
|
|
14886
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();
|
|
14887
14024
|
continue;
|
|
14888
14025
|
}
|
|
14889
14026
|
// #3455 (D3455-010) — did the server keep the document schema it
|
|
@@ -14946,8 +14083,15 @@ What a push guarantees (two stages):
|
|
|
14946
14083
|
}
|
|
14947
14084
|
// #3798 — the agent prompts, now that every function they name that
|
|
14948
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.
|
|
14949
14089
|
if (deferredAgentPromptFiles.length > 0) {
|
|
14950
|
-
|
|
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);
|
|
14951
14095
|
}
|
|
14952
14096
|
// Process group type configs
|
|
14953
14097
|
const groupTypeConfigsDir = join(configDir, "group-type-configs");
|
|
@@ -17288,12 +16432,9 @@ What a push guarantees (two stages):
|
|
|
17288
16432
|
// Blob buckets — #2880: the list row is the record. #4116: a bucket
|
|
17289
16433
|
// names its rule set, so its rows take the type configs' gate.
|
|
17290
16434
|
compareByDefinition("blob-bucket", "blobBuckets", localBlobBucketsParsed, new Map((blobBucketsDiffResult.items || []).map((b) => [b.bucketKey, b])), blobBucketsOutcomeDiff.ok && ruleSetsOk);
|
|
17291
|
-
// database-type-configs: the
|
|
17292
|
-
//
|
|
17293
|
-
//
|
|
17294
|
-
// "content not compared" rather than aborting the whole diff (#1485
|
|
17295
|
-
// edge case). Both operations AND subscriptions are included so an
|
|
17296
|
-
// 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.)
|
|
17297
16438
|
for (const [key, parsed] of localDatabaseTypesParsed) {
|
|
17298
16439
|
const remote = remoteDatabaseTypes.get(key);
|
|
17299
16440
|
if (!remote) {
|
|
@@ -17306,23 +16447,10 @@ What a push guarantees (two stages):
|
|
|
17306
16447
|
hint = "content not compared (rule sets unavailable)";
|
|
17307
16448
|
}
|
|
17308
16449
|
else {
|
|
17309
|
-
|
|
17310
|
-
|
|
17311
|
-
|
|
17312
|
-
|
|
17313
|
-
if (!opsOutcome.ok || !subsOutcome.ok) {
|
|
17314
|
-
hint = "content not compared (fetch failed)";
|
|
17315
|
-
}
|
|
17316
|
-
else {
|
|
17317
|
-
const operations = Array.isArray(opsOutcome.value) ? opsOutcome.value : [];
|
|
17318
|
-
const subscriptions = Array.isArray(subsOutcome.value) ? subsOutcome.value : [];
|
|
17319
|
-
// The shared verdict (#3267): a Modified row names the
|
|
17320
|
-
// operation, subscription, model field or `[type]` scalar it
|
|
17321
|
-
// differs in, and a comparison that throws degrades this one
|
|
17322
|
-
// row to "not compared". The ops/subscriptions are the remote
|
|
17323
|
-
// side's extra only — the file already carries its own.
|
|
17324
|
-
({ status, hint } = compareLocalToRemote(configDiffSpec("database-type-config"), parsed, remote, diffMaps, { operations, subscriptions }));
|
|
17325
|
-
}
|
|
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, {}));
|
|
17326
16454
|
}
|
|
17327
16455
|
differences.push({ type: "database-type-config", key, status, hint });
|
|
17328
16456
|
}
|
|
@@ -17467,18 +16595,11 @@ What a push guarantees (two stages):
|
|
|
17467
16595
|
// verbatim, so the two commands read identically — replaces whatever
|
|
17468
16596
|
// the comparison had to say about the file.
|
|
17469
16597
|
// #3320 — the function checks join them. Same posture, same messages:
|
|
17470
|
-
// a function file whose `entry`,
|
|
17471
|
-
// signing-secret the push preflight refuses is not
|
|
17472
|
-
// calls Synced, Modified or "local only — new".
|
|
16598
|
+
// a function file whose `entry`, retired runtime keys (#3188),
|
|
16599
|
+
// triggers or webhook signing-secret the push preflight refuses is not
|
|
16600
|
+
// a row this command calls Synced, Modified or "local only — new".
|
|
17473
16601
|
// Merged first: a file both collectors reject keeps every message push
|
|
17474
16602
|
// prints, rather than one collector's hint overwriting the other's.
|
|
17475
|
-
// #3482 — the same advisory `config push` prints, so an author sees
|
|
17476
|
-
// the retired key before they push rather than only during it. Not a
|
|
17477
|
-
// row status: the file is pushable, and saying otherwise would make
|
|
17478
|
-
// `config diff` disagree with the push it predicts.
|
|
17479
|
-
for (const message of collectFunctionTomlIgnoredKeyWarnings(configDir)) {
|
|
17480
|
-
warn(` ${message}`);
|
|
17481
|
-
}
|
|
17482
16603
|
for (const rejected of mergeConfigFileValidationErrors([
|
|
17483
16604
|
...collectConfigFileValidationErrors(configDir),
|
|
17484
16605
|
...collectFunctionTomlValidationErrors(configDir),
|
|
@@ -17777,8 +16898,13 @@ What a push guarantees (two stages):
|
|
|
17777
16898
|
// next, so an in-loop abort would leave earlier files rewritten. Every
|
|
17778
16899
|
// offending file is reported, so the author fixes the tree once.
|
|
17779
16900
|
const retiredKeyErrors = [];
|
|
16901
|
+
const databaseTypeSurface = CONFIG_SURFACES.find((surface) => surface.label === "database-type-config");
|
|
17780
16902
|
for (const file of dbFiles) {
|
|
17781
|
-
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);
|
|
17782
16908
|
if (message)
|
|
17783
16909
|
retiredKeyErrors.push(message);
|
|
17784
16910
|
}
|
|
@@ -17796,30 +16922,9 @@ What a push guarantees (two stages):
|
|
|
17796
16922
|
const filePath = join(dbTypesDir, file);
|
|
17797
16923
|
const rawBefore = readFileSync(filePath, "utf-8");
|
|
17798
16924
|
const tomlData = parseTomlFile(filePath);
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
let typeConfig;
|
|
17802
|
-
let operations;
|
|
17803
|
-
let subscriptions;
|
|
17804
|
-
try {
|
|
17805
|
-
({ typeConfig, operations, subscriptions } =
|
|
17806
|
-
parseDatabaseTypeToml(tomlData));
|
|
17807
|
-
}
|
|
17808
|
-
catch (err) {
|
|
17809
|
-
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
17810
|
-
error(` ${file}: ${err.message}`);
|
|
17811
|
-
error(`Aborting migrate-toml: subscription \`access\`/\`accessRule\` conflict in ${file}.`);
|
|
17812
|
-
process.exit(1);
|
|
17813
|
-
}
|
|
17814
|
-
throw err;
|
|
17815
|
-
}
|
|
17816
|
-
// Force-native: ignore existing hints so every TOMLable field
|
|
17817
|
-
// ends up in nested-table form. Subscriptions are preserved on the
|
|
17818
|
-
// rewrite (issue #803) so migrate-toml never drops them.
|
|
17819
|
-
const rewritten = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
17820
|
-
defaultForm: "native",
|
|
16925
|
+
const { typeConfig } = parseDatabaseTypeToml(tomlData);
|
|
16926
|
+
const rewritten = serializeDatabaseType(typeConfig, ruleSetIdToName, {
|
|
17821
16927
|
logger: (msg) => info(` ${msg}`),
|
|
17822
|
-
subscriptions,
|
|
17823
16928
|
});
|
|
17824
16929
|
if (rewritten === rawBefore) {
|
|
17825
16930
|
unchangedCount++;
|