From 2fe62c40542a5e9fea9fd6637f45aeed0f43c3f8 Mon Sep 17 00:00:00 2001 From: Steven Klaiber-Noble Date: Thu, 6 Aug 2026 16:41:48 -0700 Subject: [PATCH 1/2] Expose every common entry field, and unbreak the Homebrew CI step Two independent fixes. ## Spec 2.3a: common fields are not the operation's to decide `getFields` walked the source operation's `entry` object and exposed the fields it bound to a variable, so a payload's surface was whatever the CLI version that generated the operation happened to write. Measured across the committed fixtures: | field | template client | edge-case client | | --- | --- | --- | | posted | 9/9 | 1/5 | | description | 0/9 | 1/5 | | tags, groups, conditions | 0/9 | 0/5 | So `tags`, `groups` and `conditions` were reachable from no payload at all, and `description` from one. That is what spec 2.3a forbids in as many words: the operation is a codegen input, never the transport, and a payload travels as an `AddLedgerEntryInput`, so what the operation binds places no limit on what the payload may carry. Deriving the set invents a restriction the API does not have and moves a payload's surface whenever the CLI changes -- the spec 2.6 breakage the whole design is meant to prevent. `COMMON_ENTRY_FIELDS` now names the five, with the types `LedgerEntryInput` declares, and `getFields` appends any the operation did not already bind. Appended rather than interleaved, so the operation's own fields keep their source order. `lines` stays excluded: it cannot be combined with an entry that has a `type`. Two existing tests asserted the old rule and have been updated, and a new one derives a payload from an operation binding none of the five and asserts all five are still settable -- which is what proves they are not derived. ## Homebrew `integration-tests` has been failing on every run since the formula moved: ==> Pouring ca-certificates--2026-07-16.all.bottle.2.tar.gz ##[error]unknown install step: run You have disabled automatic updates and have not updated today. The workflow caches `/home/linuxbrew/.linuxbrew` under a fixed key, so it restores whatever Homebrew was current when that key was first written, and the runner disables auto-update. A months-old Homebrew cannot pour a current bottle. Added `brew update` before installing, which is Homebrew's own advice in that message, and rotated the cache key so the poisoned cache cannot persist. The step dies before any test runs, so this was never about the PR's code -- but it does mean `addLedgerEntries commits a batch of typed payloads` has still never executed anywhere. --- .github/workflows/tests.yml | 8 +++- src/typedBatchEntries.ts | 39 +++++++++++++++++++ .../typed-batch-entries.test.ts.snap | 33 ++++++++++++++++ tests/typed-batch-entries.test.ts | 39 ++++++++++++++++--- 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b3113e2..877a77d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,8 @@ jobs: path: | /home/linuxbrew/.linuxbrew ~/.cache/Homebrew - key: ${{ runner.os }}-homebrew-fragment-cli + # v2: the previous key holds a Homebrew too old to pour current bottles. + key: ${{ runner.os }}-homebrew-fragment-cli-v2 restore-keys: | ${{ runner.os }}-homebrew- @@ -63,6 +64,11 @@ jobs: fi echo "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" >> $GITHUB_ENV eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + # The cache restores whatever Homebrew was current when the key was first + # written, and the runner disables auto-update. A months-old Homebrew + # cannot pour a current bottle -- it fails with `unknown install step: + # run` -- so bring Homebrew itself up to date before installing. + /home/linuxbrew/.linuxbrew/bin/brew update --quiet /home/linuxbrew/.linuxbrew/bin/brew tap fragment-dev/tap || true if ! /home/linuxbrew/.linuxbrew/bin/brew list fragment-dev/tap/fragment-cli &>/dev/null; then /home/linuxbrew/.linuxbrew/bin/brew install fragment-dev/tap/fragment-cli diff --git a/src/typedBatchEntries.ts b/src/typedBatchEntries.ts index 98c9e41..b55c595 100644 --- a/src/typedBatchEntries.ts +++ b/src/typedBatchEntries.ts @@ -17,6 +17,7 @@ */ import { Kind, + parseType, valueFromASTUntyped, type DocumentNode, type ListTypeNode, @@ -114,6 +115,28 @@ const defaultWarn: Warn = (message) => { */ const DERIVED_ENTRY_FIELDS = ["type", "typeVersion", "parameters"]; +/** + * The `LedgerEntryInput` fields every payload exposes, whatever its operation + * binds, with their declared types (spec 2.3a). + * + * Deliberately not derived from the operation. An operation binds only the entry + * fields the CLI version that generated it chose to expose, and that choice has + * already changed between versions -- so deriving the set would invent a + * restriction the API does not have, and would move a payload's surface whenever + * the CLI changed. A payload travels as an `AddLedgerEntryInput`, so what the + * operation binds places no limit on what the payload may carry. + * + * `ik` and `ledgerIk` are always present already. `lines` is excluded: it cannot + * be combined with an entry that has a `type`. + */ +const COMMON_ENTRY_FIELDS: ReadonlyArray<{ name: string; type: string }> = [ + { name: "posted", type: "DateTime" }, + { name: "description", type: "String" }, + { name: "tags", type: "[LedgerEntryTagInput!]" }, + { name: "groups", type: "[LedgerEntryGroupInput!]" }, + { name: "conditions", type: "[LedgerEntryConditionInput!]" }, +]; + const findObjectField = (object: ObjectValueNode, name: string) => object.fields.find((field) => field.name.value === name); @@ -265,6 +288,22 @@ const getFields = ( }); }); + // Spec 2.3a: every payload carries these, whether or not its operation binds + // them. Appended rather than interleaved, so the operation's own fields keep + // their source order. + COMMON_ENTRY_FIELDS.forEach(({ name, type }) => { + if (fields.some((field) => field.name === name)) { + return; + } + fields.push({ + name, + wireName: name, + source: "variable", + type: parseType(type), + required: false, + }); + }); + return fields; }; diff --git a/tests/__snapshots__/typed-batch-entries.test.ts.snap b/tests/__snapshots__/typed-batch-entries.test.ts.snap index 27b6144..385d3b8 100644 --- a/tests/__snapshots__/typed-batch-entries.test.ts.snap +++ b/tests/__snapshots__/typed-batch-entries.test.ts.snap @@ -30,6 +30,12 @@ export type UserFundsAccountV1 = { ik: Scalars['SafeString']['input']; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk: Scalars['SafeString']['input']; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; }; @@ -40,10 +46,15 @@ export const userFundsAccountV1 = ( input: UserFundsAccountV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, }, + ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'user-funds-account', typeVersion: 1, }, @@ -61,6 +72,12 @@ export type UserFundsAccountV2 = { ik: Scalars['SafeString']['input']; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk: Scalars['SafeString']['input']; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; feeAmount: Scalars['Int64']['input']; @@ -73,12 +90,17 @@ export const userFundsAccountV2 = ( input: UserFundsAccountV2, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, feeAmount: input.parameters.feeAmount, ...(input.parameters.memo !== undefined && { memo: input.parameters.memo }), }, + ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'user-funds-account', typeVersion: 2, }, @@ -96,6 +118,12 @@ export type RuntimeThingV1 = { ik: Scalars['SafeString']['input']; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk: Scalars['SafeString']['input']; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; /** * This entry type's operation does not bind its parameters to typed * variables, so they cannot be typed individually. @@ -108,8 +136,13 @@ export const runtimeThingV1 = ( input: RuntimeThingV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: input.parameters, + ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'runtime-thing', typeVersion: 1, }, diff --git a/tests/typed-batch-entries.test.ts b/tests/typed-batch-entries.test.ts index cf94878..12a162b 100644 --- a/tests/typed-batch-entries.test.ts +++ b/tests/typed-batch-entries.test.ts @@ -139,6 +139,9 @@ describe("recognition", () => { }); }); +/** Spec 2.3a: on every payload, whatever its operation binds. */ +const COMMON_FIELD_NAMES = ["posted", "description", "tags", "groups", "conditions"]; + describe("identity", () => { it("keeps two versions of one entry type apart", () => { const payloads = derive( @@ -212,7 +215,10 @@ describe("identity", () => { ); expect(payloads).toHaveLength(1); - expect(payloads[0].fields.map((field) => field.name)).toEqual(["ledgerIk"]); + expect(payloads[0].fields.map((field) => field.name)).toEqual([ + "ledgerIk", + ...COMMON_FIELD_NAMES, + ]); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls[0][0]).toContain("different entry fields"); }); @@ -504,7 +510,7 @@ describe("rendering", () => { ); }); - it("exposes exactly the entry fields the operation binds", () => { + it("exposes the entry fields the operation binds", () => { const output = generate( entryOperation({ parameters: "posted: $posted, tags: $tags, parameters: {amount: $amount}", @@ -518,10 +524,31 @@ describe("rendering", () => { expect(payload).toContain("ledgerIk: Scalars['SafeString']['input'];"); expect(payload).toContain("posted?: Scalars['DateTime']['input'] | undefined;"); expect(payload).toContain("tags?: Array | undefined;"); - // Nothing the operation leaves out is offered to the caller. - expect(payload).not.toContain("groups"); - expect(payload).not.toContain("conditions"); - expect(payload).not.toContain("description"); + // `lines` cannot be combined with an entry that has a `type`, so it is the + // one LedgerEntryInput field a payload never offers unasked. + expect(payload).not.toContain("lines"); + }); + + it("exposes every common field even when the operation binds none of them", () => { + // Spec 2.3a: the common fields are fixed by `LedgerEntryInput`, not derived + // from the operation. A CLI that stops binding `tags` must not silently + // remove `tags` from the payload, and no CLI generation binds `description` + // at all -- so deriving the set would leave it permanently unreachable. + const output = generate( + entryOperation({ + parameters: "parameters: {amount: $amount}", + variables: "$ik: SafeString!, $ledgerIk: SafeString!, $amount: String!", + }), + ); + + const payload = payloadType(output, "ThingV1"); + expect(payload).toContain("posted?: Scalars['DateTime']['input'] | undefined;"); + expect(payload).toContain("description?: Scalars['String']['input'] | undefined;"); + expect(payload).toContain("tags?: Array | undefined;"); + expect(payload).toContain("groups?: Array | undefined;"); + expect(payload).toContain( + "conditions?: Array | undefined;", + ); expect(payload).not.toContain("lines"); }); From aa08aa652c4db8c47d9cf51c20355a26f268e5d6 Mon Sep 17 00:00:00 2001 From: Steven Klaiber-Noble Date: Thu, 6 Aug 2026 16:45:36 -0700 Subject: [PATCH 2/2] Regenerate the fixtures, and stop the tests asserting the old rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed clients under `tests/fixtures/` are generated output, and the previous commit changed the generator without regenerating them. They were stale in a way that mattered: `batch-ledger-entries.test.ts` imports its builders from them, so those tests were exercising pre-fix code, and `template-schema-client.test.ts` reads them as text and passed only because of it. Regenerated with `scripts/update-test-schema.sh`'s codegen steps. Every payload now carries the five common fields: | field | template client | edge-case client | | --- | --- | --- | | posted | 9/9 | 5/5 | | description | 9/9 | 4/5 | | tags | 9/9 | 4/5 | | groups | 9/9 | 5/5 | | conditions | 9/9 | 5/5 | The 4/5 is `FixedValuesV1`, whose operation pins `description` and `tags` to literals. A field the operation fixes is still posted but stays out of the caller's hands, which is the same rule spec 2.3 applies to parameters — exposing it would let a caller override a value the operation deliberately set. Two tests asserted the behaviour this stack is removing, and both passed only against the stale fixtures: - `exposes the fields each operation set binds, and no others` asserted that the plain operation set's payload has no `tags`, `groups` or `conditions`. Those two operation sets come from different CLI generations, which is exactly the drift spec 2.3a exists to hide from callers, so the test now asserts the opposite: the same surface whichever generation produced it. - `binds different fields per entry type within one operation set` asserted `CardSettleV1` has no `conditions` because its operation does not bind them. Now it does, like its siblings. 74 passing; the 13 failures are the credential-gated integration tests, unchanged from before this work. --- tests/fixtures/generated-edge-case-client.ts | 46 ++++++++++++ tests/fixtures/generated-template-client.ts | 72 +++++++++++++++++++ .../generated-template-runtime-args-client.ts | 28 ++++++++ tests/fixtures/generated-test-client.ts | 32 +++++++++ tests/template-schema-client.test.ts | 42 +++++------ 5 files changed, 200 insertions(+), 20 deletions(-) diff --git a/tests/fixtures/generated-edge-case-client.ts b/tests/fixtures/generated-edge-case-client.ts index 6a87c15..31cfe39 100644 --- a/tests/fixtures/generated-edge-case-client.ts +++ b/tests/fixtures/generated-edge-case-client.ts @@ -3493,6 +3493,9 @@ export type RuntimeLinesV1 = { description?: Scalars['String']['input'] | undefined; /** The Ledger Lines to create, for entry types whose lines the Schema does not fix. */ lines: Array; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; }; /** Builds an `addLedgerEntries` entry for `runtime_lines` (typeVersion 1). */ @@ -3500,10 +3503,13 @@ export const runtimeLinesV1 = ( input: RuntimeLinesV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, lines: input.lines, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'runtime_lines', typeVersion: 1, }, @@ -3521,6 +3527,12 @@ export type UntypedParametersV1 = { ik: Scalars['SafeString']['input']; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk: Scalars['SafeString']['input']; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; /** * This entry type's operation does not bind its parameters to typed * variables, so they cannot be typed individually. @@ -3533,8 +3545,13 @@ export const untypedParametersV1 = ( input: UntypedParametersV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: input.parameters, + ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'untyped_parameters', typeVersion: 1, }, @@ -3552,6 +3569,12 @@ export type AllOptionalV2 = { ik: Scalars['SafeString']['input']; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk: Scalars['SafeString']['input']; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters?: { memo?: Scalars['String']['input'] | undefined; note?: Scalars['String']['input'] | undefined; @@ -3568,8 +3591,13 @@ export const allOptionalV2 = ( }; return { entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, ...(Object.keys(parameters).length > 0 && { parameters }), + ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'all_optional', typeVersion: 2, }, @@ -3589,6 +3617,12 @@ export type EitherLedgerKeyV1 = { ledgerId?: Scalars['ID']['input'] | undefined; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk?: Scalars['SafeString']['input'] | undefined; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; }; @@ -3604,10 +3638,15 @@ export const eitherLedgerKeyV1 = ( }; return { entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ...(Object.keys(ledger).length > 0 && { ledger }), parameters: { amount: input.parameters.amount, }, + ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'either_ledger_key', typeVersion: 1, }, @@ -3626,6 +3665,10 @@ export type FixedValuesV1 = { ik: Scalars['SafeString']['input']; /** The Idempotency Key of the Ledger to add this Ledger Entry to. */ ledgerIk: Scalars['SafeString']['input']; + /** ISO 8601 timestamp to post this Ledger Entry at. */ + posted?: Scalars['DateTime']['input'] | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; }; @@ -3636,12 +3679,15 @@ export const fixedValuesV1 = ( input: FixedValuesV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), description: 'posted by the nightly sweep', + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, currency: 'USD', }, + ...(input.posted !== undefined && { posted: input.posted }), tags: [{ key: 'source', value: 'sweep' }], type: 'fixed_values', typeVersion: 1, diff --git a/tests/fixtures/generated-template-client.ts b/tests/fixtures/generated-template-client.ts index 36c3f97..db889c7 100644 --- a/tests/fixtures/generated-template-client.ts +++ b/tests/fixtures/generated-template-client.ts @@ -4011,6 +4011,10 @@ export type OrderPlacedV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { user_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4028,6 +4032,9 @@ export const orderPlacedV1 = ( input: OrderPlacedV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { user_id: input.parameters.user_id, @@ -4040,6 +4047,7 @@ export const orderPlacedV1 = ( driver_id: input.parameters.driver_id, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'order_placed', typeVersion: 1, }, @@ -4059,6 +4067,10 @@ export type OrderPlacedV2 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { user_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4077,6 +4089,9 @@ export const orderPlacedV2 = ( input: OrderPlacedV2, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { user_id: input.parameters.user_id, @@ -4090,6 +4105,7 @@ export const orderPlacedV2 = ( driver_id: input.parameters.driver_id, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'order_placed', typeVersion: 2, }, @@ -4109,6 +4125,10 @@ export type CardSettleV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { user_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4122,6 +4142,9 @@ export const cardSettleV1 = ( input: CardSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { user_id: input.parameters.user_id, @@ -4130,6 +4153,7 @@ export const cardSettleV1 = ( amount: input.parameters.amount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'card_settle', typeVersion: 1, }, @@ -4149,6 +4173,10 @@ export type RestaurantPayoutInitiateV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { restaurant_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4163,6 +4191,9 @@ export const restaurantPayoutInitiateV1 = ( input: RestaurantPayoutInitiateV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { restaurant_id: input.parameters.restaurant_id, @@ -4172,6 +4203,7 @@ export const restaurantPayoutInitiateV1 = ( payout_id: input.parameters.payout_id, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'restaurant_payout_initiate', typeVersion: 1, }, @@ -4191,6 +4223,10 @@ export type RestaurantPayoutSettleV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { restaurant_id: Scalars['String']['input']; payout_id: Scalars['String']['input']; @@ -4204,6 +4240,9 @@ export const restaurantPayoutSettleV1 = ( input: RestaurantPayoutSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { restaurant_id: input.parameters.restaurant_id, @@ -4212,6 +4251,7 @@ export const restaurantPayoutSettleV1 = ( amount: input.parameters.amount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'restaurant_payout_settle', typeVersion: 1, }, @@ -4231,6 +4271,10 @@ export type DriverPayoutInitiateV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { driver_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4245,6 +4289,9 @@ export const driverPayoutInitiateV1 = ( input: DriverPayoutInitiateV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { driver_id: input.parameters.driver_id, @@ -4254,6 +4301,7 @@ export const driverPayoutInitiateV1 = ( payout_id: input.parameters.payout_id, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'driver_payout_initiate', typeVersion: 1, }, @@ -4273,6 +4321,10 @@ export type DriverPayoutSettleV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { driver_id: Scalars['String']['input']; payout_id: Scalars['String']['input']; @@ -4286,6 +4338,9 @@ export const driverPayoutSettleV1 = ( input: DriverPayoutSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { driver_id: input.parameters.driver_id, @@ -4294,6 +4349,7 @@ export const driverPayoutSettleV1 = ( amount: input.parameters.amount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'driver_payout_settle', typeVersion: 1, }, @@ -4313,6 +4369,10 @@ export type DisputePayoutInitiateV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { user_id: Scalars['String']['input']; disputes_id: Scalars['String']['input']; @@ -4328,6 +4388,9 @@ export const disputePayoutInitiateV1 = ( input: DisputePayoutInitiateV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { user_id: input.parameters.user_id, @@ -4338,6 +4401,7 @@ export const disputePayoutInitiateV1 = ( order_id: input.parameters.order_id, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'dispute_payout_initiate', typeVersion: 1, }, @@ -4357,6 +4421,10 @@ export type DisputePayoutSettleV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { user_id: Scalars['String']['input']; disputes_id: Scalars['String']['input']; @@ -4371,6 +4439,9 @@ export const disputePayoutSettleV1 = ( input: DisputePayoutSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { user_id: input.parameters.user_id, @@ -4380,6 +4451,7 @@ export const disputePayoutSettleV1 = ( order_id: input.parameters.order_id, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'dispute_payout_settle', typeVersion: 1, }, diff --git a/tests/fixtures/generated-template-runtime-args-client.ts b/tests/fixtures/generated-template-runtime-args-client.ts index 0ceb306..5a73f17 100644 --- a/tests/fixtures/generated-template-runtime-args-client.ts +++ b/tests/fixtures/generated-template-runtime-args-client.ts @@ -4036,6 +4036,7 @@ export type OrderPlacedV1 = { tags?: Array | undefined; groups?: Array | undefined; conditions?: Array | undefined; + description?: Scalars['String']['input'] | undefined; parameters: { user_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4054,6 +4055,7 @@ export const orderPlacedV1 = ( ): AddLedgerEntryInput => ({ entry: { ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4090,6 +4092,7 @@ export type OrderPlacedV2 = { tags?: Array | undefined; groups?: Array | undefined; conditions?: Array | undefined; + description?: Scalars['String']['input'] | undefined; parameters: { user_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4109,6 +4112,7 @@ export const orderPlacedV2 = ( ): AddLedgerEntryInput => ({ entry: { ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4145,6 +4149,8 @@ export type CardSettleV1 = { posted?: Scalars['DateTime']['input'] | undefined; tags?: Array | undefined; groups?: Array | undefined; + description?: Scalars['String']['input'] | undefined; + conditions?: Array | undefined; parameters: { user_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4158,6 +4164,8 @@ export const cardSettleV1 = ( input: CardSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4189,6 +4197,8 @@ export type RestaurantPayoutInitiateV1 = { posted?: Scalars['DateTime']['input'] | undefined; tags?: Array | undefined; groups?: Array | undefined; + description?: Scalars['String']['input'] | undefined; + conditions?: Array | undefined; parameters: { restaurant_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4203,6 +4213,8 @@ export const restaurantPayoutInitiateV1 = ( input: RestaurantPayoutInitiateV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4235,6 +4247,8 @@ export type RestaurantPayoutSettleV1 = { posted?: Scalars['DateTime']['input'] | undefined; tags?: Array | undefined; groups?: Array | undefined; + description?: Scalars['String']['input'] | undefined; + conditions?: Array | undefined; parameters: { restaurant_id: Scalars['String']['input']; payout_id: Scalars['String']['input']; @@ -4248,6 +4262,8 @@ export const restaurantPayoutSettleV1 = ( input: RestaurantPayoutSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4279,6 +4295,8 @@ export type DriverPayoutInitiateV1 = { posted?: Scalars['DateTime']['input'] | undefined; tags?: Array | undefined; groups?: Array | undefined; + description?: Scalars['String']['input'] | undefined; + conditions?: Array | undefined; parameters: { driver_id: Scalars['String']['input']; order_id: Scalars['String']['input']; @@ -4293,6 +4311,8 @@ export const driverPayoutInitiateV1 = ( input: DriverPayoutInitiateV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4325,6 +4345,8 @@ export type DriverPayoutSettleV1 = { posted?: Scalars['DateTime']['input'] | undefined; tags?: Array | undefined; groups?: Array | undefined; + description?: Scalars['String']['input'] | undefined; + conditions?: Array | undefined; parameters: { driver_id: Scalars['String']['input']; payout_id: Scalars['String']['input']; @@ -4338,6 +4360,8 @@ export const driverPayoutSettleV1 = ( input: DriverPayoutSettleV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4370,6 +4394,7 @@ export type DisputePayoutInitiateV1 = { tags?: Array | undefined; groups?: Array | undefined; conditions?: Array | undefined; + description?: Scalars['String']['input'] | undefined; parameters: { user_id: Scalars['String']['input']; disputes_id: Scalars['String']['input']; @@ -4386,6 +4411,7 @@ export const disputePayoutInitiateV1 = ( ): AddLedgerEntryInput => ({ entry: { ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { @@ -4420,6 +4446,7 @@ export type DisputePayoutSettleV1 = { tags?: Array | undefined; groups?: Array | undefined; conditions?: Array | undefined; + description?: Scalars['String']['input'] | undefined; parameters: { user_id: Scalars['String']['input']; disputes_id: Scalars['String']['input']; @@ -4435,6 +4462,7 @@ export const disputePayoutSettleV1 = ( ): AddLedgerEntryInput => ({ entry: { ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { diff --git a/tests/fixtures/generated-test-client.ts b/tests/fixtures/generated-test-client.ts index 3c7d8f9..1a28f9f 100644 --- a/tests/fixtures/generated-test-client.ts +++ b/tests/fixtures/generated-test-client.ts @@ -3641,6 +3641,10 @@ export type UserFundsAccountV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; }; @@ -3651,11 +3655,15 @@ export const userFundsAccountV1 = ( input: UserFundsAccountV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'user-funds-account', typeVersion: 1, }, @@ -3675,6 +3683,10 @@ export type UserFundsAccountV2 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; feeAmount: Scalars['String']['input']; @@ -3686,12 +3698,16 @@ export const userFundsAccountV2 = ( input: UserFundsAccountV2, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, feeAmount: input.parameters.feeAmount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'user-funds-account', typeVersion: 2, }, @@ -3711,6 +3727,10 @@ export type FundingSettlementV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; }; @@ -3721,11 +3741,15 @@ export const fundingSettlementV1 = ( input: FundingSettlementV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'fundingSettlement', typeVersion: 1, }, @@ -3745,6 +3769,10 @@ export type PaymentProcessingV1 = { ledgerIk: Scalars['SafeString']['input']; /** ISO 8601 timestamp to post this Ledger Entry at. */ posted?: Scalars['DateTime']['input'] | undefined; + description?: Scalars['String']['input'] | undefined; + tags?: Array | undefined; + groups?: Array | undefined; + conditions?: Array | undefined; parameters: { amount: Scalars['String']['input']; }; @@ -3755,11 +3783,15 @@ export const paymentProcessingV1 = ( input: PaymentProcessingV1, ): AddLedgerEntryInput => ({ entry: { + ...(input.conditions !== undefined && { conditions: input.conditions }), + ...(input.description !== undefined && { description: input.description }), + ...(input.groups !== undefined && { groups: input.groups }), ledger: { ik: input.ledgerIk }, parameters: { amount: input.parameters.amount, }, ...(input.posted !== undefined && { posted: input.posted }), + ...(input.tags !== undefined && { tags: input.tags }), type: 'payment_processing', typeVersion: 1, }, diff --git a/tests/template-schema-client.test.ts b/tests/template-schema-client.test.ts index 6511edd..1a3f519 100644 --- a/tests/template-schema-client.test.ts +++ b/tests/template-schema-client.test.ts @@ -58,42 +58,44 @@ const generatedRuntimeArgsClient = read( ); describe("generated template client", () => { - it("exposes the fields each operation set binds, and no others", () => { + it("exposes the same common fields whichever operation set generated it", () => { const plain = payloadType(generatedClient, "DisputePayoutInitiateV1"); const runtimeArgs = payloadType( generatedRuntimeArgsClient, "DisputePayoutInitiateV1", ); - // Both operations bind the entry's Ledger and `posted`. + // These two operation sets come from different CLI generations and bind + // different entry fields -- the runtime-args one binds tags, groups and + // conditions, the plain one does not. Spec 2.3a is what makes that invisible + // to a caller: the common fields are fixed by `LedgerEntryInput`, so + // regenerating with a different CLI cannot move a payload's surface. [plain, runtimeArgs].forEach((payload) => { expect(payload).toContain("ledgerIk: Scalars['SafeString']['input'];"); expect(payload).toContain("posted?: Scalars['DateTime']['input'] | undefined;"); - }); - - // Only the runtime-args operation binds tags, groups and conditions, so only - // its payload lets a caller set them. - expect(runtimeArgs).toContain("tags?: Array | undefined;"); - expect(runtimeArgs).toContain("groups?: Array | undefined;"); - expect(runtimeArgs).toContain( - "conditions?: Array | undefined;", - ); - expect(plain).not.toContain("tags"); - expect(plain).not.toContain("groups"); - expect(plain).not.toContain("conditions"); + expect(payload).toContain("description?: Scalars['String']['input'] | undefined;"); + expect(payload).toContain("tags?: Array | undefined;"); + expect(payload).toContain("groups?: Array | undefined;"); + expect(payload).toContain( + "conditions?: Array | undefined;", + ); - // Neither binds lines for this entry type, whose lines the Schema fixes. - expect(plain).not.toContain("lines"); - expect(runtimeArgs).not.toContain("lines"); + // `lines` is the one field a payload never offers: it cannot be combined + // with an entry that has a `type`. + expect(payload).not.toContain("lines"); + }); }); - it("binds different fields per entry type within one operation set", () => { - // `card_settle` binds tags and groups but no conditions; the payload says so. + it("gives every entry type in a set the same common fields", () => { + // `card_settle` binds tags and groups but not conditions. Before spec 2.3a + // that made its payload differ from its siblings'; now it does not. const cardSettle = payloadType(generatedRuntimeArgsClient, "CardSettleV1"); expect(cardSettle).toContain("tags?: Array | undefined;"); expect(cardSettle).toContain("groups?: Array | undefined;"); - expect(cardSettle).not.toContain("conditions"); + expect(cardSettle).toContain( + "conditions?: Array | undefined;", + ); }); it("matches the payload surface derived from the template Schema", () => {