Skip to content

refactor(google): readonly-domain-types (stack 3/12, re-split #307) - #376

Closed
YosefHayim wants to merge 1 commit into
refactor/types/readonly-stack-02-applefrom
refactor/types/readonly-stack-03-google
Closed

refactor(google): readonly-domain-types (stack 3/12, re-split #307)#376
YosefHayim wants to merge 1 commit into
refactor/types/readonly-stack-02-applefrom
refactor/types/readonly-stack-03-google

Conversation

@YosefHayim

@YosefHayim YosefHayim commented Aug 7, 2026

Copy link
Copy Markdown
Owner

User description

Stack 3/12 of re-split HOLD #307

Domain: google
Base: refactor/types/readonly-stack-02-apple
Full green tip: refactor/foundation/readonly-types-full

Land stack in order. Intermediate PRs may not typecheck alone.


Summary by cubic

Refactors the Google Play clients to work with readonly domain types by constructing mutable objects at API boundaries. Adds a deep-clone helper for request bodies to satisfy the @googleapis/androidpublisher client.

  • Refactors
    • Introduced mutableGoogleRequest<T>() and applied it to track, subscription (create/update), and offer requests so the Google client receives plain mutable objects.
    • Normalized Google responses into MutableDeep models throughout the Play client and reporting (e.g., reviews, releases, products, base plans, subscriptions, offers, replies, availability, converted prices, vitals rows).

Written for commit 64a71e5. Summary will update on new commits.

Review in cubic


CodeAnt-AI Description

Keep Google Play operations compatible with read-only domain data

What Changed

  • Google Play track, subscription, and offer updates now safely accept read-only data without changing the values supplied by callers
  • Responses and normalized reporting data continue to be produced in the expected writable shape for reconciliation and updates
  • Service-account, review, availability, pricing, and vitals results remain usable with the updated domain types

Impact

✅ Reliable Google Play catalog updates
✅ Read-only data remains safe during API requests
✅ Stable subscription and reporting reconciliation

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 64a71e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codeant-ai

codeant-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 64a71e5 Aug 07, 2026 · 11:06 11:09

@codeant-ai

codeant-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Aug 7, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Google: adapt Play clients to readonly domain types via mutable request/normalize shapes

✨ Enhancement 🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Update Google domain consumers to work with readonly domain types via MutableDeep.
• Deep-clone request bodies into plain mutable objects for Google’s generated API client.
• Ensure normalization helpers can build objects incrementally without readonly assignment errors.
Diagram

graph TD
  A["Readonly domain types"] --> B["Google Play clients"] --> C["MutableDeep builders"] --> D["Normalized domain objects"]
  B --> E["mutableGoogleRequest"] --> F["Google generated API"]
  subgraph Legend
    direction LR
    _dom[Domain types] ~~~ _code[Client code] ~~~ _ext[[External API]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use structuredClone instead of JSON stringify/parse
  • ➕ Preserves more JS types (e.g., Dates) and avoids JSON limitations
  • ➕ Avoids accidental loss of undefined fields due to JSON serialization
  • ➖ Runtime/environment support constraints depending on target Node version
  • ➖ Still a deep copy; may be slower or require polyfill
2. Build request DTOs explicitly (no deep clone)
  • ➕ No runtime cloning; clearer control over what is sent to Google
  • ➕ Avoids surprising serialization changes and silently dropped fields
  • ➖ More verbose and repetitive across many request shapes
  • ➖ Higher maintenance cost as Google schemas evolve
3. Isolate mutability at boundary with dedicated mappers
  • ➕ Keeps domain layer purely readonly while keeping API layer mutable
  • ➕ Centralizes conversion logic for consistency and testing
  • ➖ Additional abstraction layer for a stack PR series
  • ➖ May be overkill if only a few endpoints require mutability

Recommendation: The PR’s approach (MutableDeep for incremental construction + cloning only at the Google client boundary) is a pragmatic fit for a stacked readonly migration. If request bodies start carrying non-JSON-safe values or subtle field-dropping becomes a concern, prefer explicit DTO builders or structuredClone (where available) for safer semantics.

Files changed (2) +48 / -25

Refactor (2) +48 / -25
playClient.tsUse MutableDeep for normalizers and deep-clone request bodies for Google client +46/-24

Use MutableDeep for normalizers and deep-clone request bodies for Google client

• Switches normalization helpers to construct values as MutableDeep<T> to satisfy readonly domain typing while still building objects incrementally. Adds mutableGoogleRequest() (JSON deep-clone) and uses it when sending requestBody to androidpublisher endpoints. Updates service account parsing and various normalized result objects to be built as MutableDeep.

src/google/playClient.ts

playReporting.tsAdjust vitals normalization to build rows using MutableDeep +2/-1

Adjust vitals normalization to build rows using MutableDeep

• Imports MutableDeep and uses it for PlayVitalsRow construction inside metric normalization, aligning reporting client code with readonly domain types.

src/google/playReporting.ts

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Unchecked request cast 🐞 Bug ⚙ Maintainability
Description
mutableGoogleRequest accepts unknown but returns the caller-selected generic type, so request
bodies are no longer type-checked against the googleapis schema types at the call site. This can
allow an incompatible request shape to compile and only fail at runtime in write paths
(tracks/subscriptions/offers).
Code

src/google/playClient.ts[R10-13]

+/** Deep-clone a Launch boundary value into a mutable plain object for Google's generated client. */
+const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
+  const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));
+  return clonedRequest;
Evidence
The helper’s unknown input combined with a generic return type removes compile-time verification
that the argument matches the requested Google schema type; the PR then uses explicit schema type
parameters at requestBody call sites, which will compile even if the provided object is not
assignable to that schema.

src/google/playClient.ts[10-13]
src/google/playClient.ts[656-668]
src/google/playClient.ts[883-896]
src/google/playClient.ts[965-985]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mutableGoogleRequest` currently has the signature `<T>(requestShape: unknown) => T`, which turns it into an unchecked cast: callers can request any return type regardless of the actual argument shape.

### Issue Context
This helper is used to build `requestBody` objects for googleapis write calls (tracks/subscriptions/offers). Keeping the deep-clone behavior is fine, but we should preserve compiler checking that the provided object is actually assignable to the intended Google schema.

### Fix Focus Areas
- src/google/playClient.ts[10-13]
- src/google/playClient.ts[656-668]
- src/google/playClient.ts[883-896]
- src/google/playClient.ts[965-985]

### Suggested fix
1. Change the helper to be type-safe at the input:
  - e.g. `const mutableGoogleRequest = <T>(requestShape: T): MutableDeep<T> => structuredClone(requestShape) as MutableDeep<T>;`
  - (If `structuredClone` is not desired, keep JSON clone but still type the parameter as `T`, not `unknown`, and cast internally.)
2. At call sites, make the argument satisfy the Google schema type so TS checks it:
  - `requestBody: mutableGoogleRequest({ ... } satisfies androidpublisher_v3.Schema$Subscription)`
  - or assign the object to a `const body: androidpublisher_v3.Schema$Subscription = { ... }` before cloning.

This preserves the “mutable plain object” goal while preventing accidental schema drift from compiling silently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Brittle JSON deep clone 🐞 Bug ☼ Reliability
Description
mutableGoogleRequest deep-clones via JSON.parse(JSON.stringify(...)), which will throw or change
values if non-JSON-safe data ever reaches a request body (e.g., circular references, BigInt, or
undefined array entries becoming null). That creates a sharp edge on the Google write paths that
now always route through this helper.
Code

src/google/playClient.ts[R11-13]

+const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
+  const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));
+  return clonedRequest;
Evidence
The helper explicitly clones by JSON stringify/parse, and the PR updates multiple write endpoints to
construct their requestBody via this helper, so any future non-JSON-safe value reaching these
request shapes will fail at clone time.

src/google/playClient.ts[10-13]
src/google/playClient.ts[656-668]
src/google/playClient.ts[883-896]
src/google/playClient.ts[965-985]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The helper clones request bodies using JSON serialization, which is inherently lossy and can throw for certain runtime values.

### Issue Context
Even if current request bodies are intended to be JSON-shaped, this helper is now the standard pathway for several write operations; using a non-lossy clone reduces fragility if the shapes evolve.

### Fix Focus Areas
- src/google/playClient.ts[10-13]

### Suggested fix
Replace JSON cloning with `structuredClone` (Node >= 20) to avoid JSON lossy behavior:
- `const clonedRequest = structuredClone(requestShape) as MutableDeep<T>;`

If you must keep JSON cloning, consider adding an explicit error message when stringify/parse fails so failures are more diagnosable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 48 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/google/playClient.ts
Comment on lines +10 to +13
/** Deep-clone a Launch boundary value into a mutable plain object for Google's generated client. */
const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));
return clonedRequest;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Unchecked request cast 🐞 Bug ⚙ Maintainability

mutableGoogleRequest accepts unknown but returns the caller-selected generic type, so request
bodies are no longer type-checked against the googleapis schema types at the call site. This can
allow an incompatible request shape to compile and only fail at runtime in write paths
(tracks/subscriptions/offers).
Agent Prompt
### Issue description
`mutableGoogleRequest` currently has the signature `<T>(requestShape: unknown) => T`, which turns it into an unchecked cast: callers can request any return type regardless of the actual argument shape.

### Issue Context
This helper is used to build `requestBody` objects for googleapis write calls (tracks/subscriptions/offers). Keeping the deep-clone behavior is fine, but we should preserve compiler checking that the provided object is actually assignable to the intended Google schema.

### Fix Focus Areas
- src/google/playClient.ts[10-13]
- src/google/playClient.ts[656-668]
- src/google/playClient.ts[883-896]
- src/google/playClient.ts[965-985]

### Suggested fix
1. Change the helper to be type-safe at the input:
   - e.g. `const mutableGoogleRequest = <T>(requestShape: T): MutableDeep<T> => structuredClone(requestShape) as MutableDeep<T>;`
   - (If `structuredClone` is not desired, keep JSON clone but still type the parameter as `T`, not `unknown`, and cast internally.)
2. At call sites, make the argument satisfy the Google schema type so TS checks it:
   - `requestBody: mutableGoogleRequest({ ... } satisfies androidpublisher_v3.Schema$Subscription)`
   - or assign the object to a `const body: androidpublisher_v3.Schema$Subscription = { ... }` before cloning.

This preserves the “mutable plain object” goal while preventing accidental schema drift from compiling silently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/google/playClient.ts
Comment on lines +11 to +13
const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));
return clonedRequest;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Brittle json deep clone 🐞 Bug ☼ Reliability

mutableGoogleRequest deep-clones via JSON.parse(JSON.stringify(...)), which will throw or change
values if non-JSON-safe data ever reaches a request body (e.g., circular references, BigInt, or
undefined array entries becoming null). That creates a sharp edge on the Google write paths that
now always route through this helper.
Agent Prompt
### Issue description
The helper clones request bodies using JSON serialization, which is inherently lossy and can throw for certain runtime values.

### Issue Context
Even if current request bodies are intended to be JSON-shaped, this helper is now the standard pathway for several write operations; using a non-lossy clone reduces fragility if the shapes evolve.

### Fix Focus Areas
- src/google/playClient.ts[10-13]

### Suggested fix
Replace JSON cloning with `structuredClone` (Node >= 20) to avoid JSON lossy behavior:
- `const clonedRequest = structuredClone(requestShape) as MutableDeep<T>;`

If you must keep JSON cloning, consider adding an explicit error message when stringify/parse fails so failures are more diagnosable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b09ccf7-6fb7-43c2-a26e-4365592c950a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/google/playClient.ts">

<violation number="1" location="src/google/playClient.ts:11">
P2: mutableGoogleRequest takes `requestShape: unknown` and returns the caller-supplied generic type, effectively acting as an unchecked cast. Callers can pass any object shape and specify any return type (e.g. `mutableGoogleRequest<androidpublisher_v3.Schema$Subscription>({...})`), and TypeScript won't verify that the argument actually matches the requested schema. This removes compile-time safety on write paths for tracks/subscriptions/offers, allowing schema drift to compile and fail only at runtime. Consider typing the parameter as `T` (or using `satisfies` at call sites) so the compiler still verifies the input shape.</violation>

<violation number="2" location="src/google/playClient.ts:12">
P2: The new `mutableGoogleRequest` uses a `JSON.parse(JSON.stringify(...))` round-trip to produce a mutable request body for Google's generated client. Today the inputs (normalized Play releases/subscriptions/offers) are plain JSON-safe data, so nothing breaks, but this clone is lossy and unguarded: it throws a `SyntaxError` if the input is ever `undefined`, and it silently drops `undefined` fields and cannot represent `Date`/`BigInt`/circular values. Since the only requirement is satisfying the mutable client types, a `structuredClone` (which preserves values without the JSON loss) is safer; if a plain cast is acceptable that avoids the clone cost entirely.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/google/playClient.ts

/** Deep-clone a Launch boundary value into a mutable plain object for Google's generated client. */
const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new mutableGoogleRequest uses a JSON.parse(JSON.stringify(...)) round-trip to produce a mutable request body for Google's generated client. Today the inputs (normalized Play releases/subscriptions/offers) are plain JSON-safe data, so nothing breaks, but this clone is lossy and unguarded: it throws a SyntaxError if the input is ever undefined, and it silently drops undefined fields and cannot represent Date/BigInt/circular values. Since the only requirement is satisfying the mutable client types, a structuredClone (which preserves values without the JSON loss) is safer; if a plain cast is acceptable that avoids the clone cost entirely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/google/playClient.ts, line 12:

<comment>The new `mutableGoogleRequest` uses a `JSON.parse(JSON.stringify(...))` round-trip to produce a mutable request body for Google's generated client. Today the inputs (normalized Play releases/subscriptions/offers) are plain JSON-safe data, so nothing breaks, but this clone is lossy and unguarded: it throws a `SyntaxError` if the input is ever `undefined`, and it silently drops `undefined` fields and cannot represent `Date`/`BigInt`/circular values. Since the only requirement is satisfying the mutable client types, a `structuredClone` (which preserves values without the JSON loss) is safer; if a plain cast is acceptable that avoids the clone cost entirely.</comment>

<file context>
@@ -5,6 +5,13 @@ import {
+
+/** Deep-clone a Launch boundary value into a mutable plain object for Google's generated client. */
+const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
+  const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));
+  return clonedRequest;
+};
</file context>

Comment thread src/google/playClient.ts
import type { MutableDeep } from '../core/types/mutable.js';

/** Deep-clone a Launch boundary value into a mutable plain object for Google's generated client. */
const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: mutableGoogleRequest takes requestShape: unknown and returns the caller-supplied generic type, effectively acting as an unchecked cast. Callers can pass any object shape and specify any return type (e.g. mutableGoogleRequest<androidpublisher_v3.Schema$Subscription>({...})), and TypeScript won't verify that the argument actually matches the requested schema. This removes compile-time safety on write paths for tracks/subscriptions/offers, allowing schema drift to compile and fail only at runtime. Consider typing the parameter as T (or using satisfies at call sites) so the compiler still verifies the input shape.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/google/playClient.ts, line 11:

<comment>mutableGoogleRequest takes `requestShape: unknown` and returns the caller-supplied generic type, effectively acting as an unchecked cast. Callers can pass any object shape and specify any return type (e.g. `mutableGoogleRequest<androidpublisher_v3.Schema$Subscription>({...})`), and TypeScript won't verify that the argument actually matches the requested schema. This removes compile-time safety on write paths for tracks/subscriptions/offers, allowing schema drift to compile and fail only at runtime. Consider typing the parameter as `T` (or using `satisfies` at call sites) so the compiler still verifies the input shape.</comment>

<file context>
@@ -5,6 +5,13 @@ import {
+import type { MutableDeep } from '../core/types/mutable.js';
+
+/** Deep-clone a Launch boundary value into a mutable plain object for Google's generated client. */
+const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
+  const clonedRequest: GoogleRequest = JSON.parse(JSON.stringify(requestShape));
+  return clonedRequest;
</file context>
Suggested change
const mutableGoogleRequest = <GoogleRequest>(requestShape: unknown): GoogleRequest => {
const mutableGoogleRequest = <GoogleRequest>(requestShape: GoogleRequest): MutableDeep<GoogleRequest> => {
const clonedRequest: MutableDeep<GoogleRequest> = JSON.parse(JSON.stringify(requestShape));
return clonedRequest;
};

@YosefHayim

Copy link
Copy Markdown
Owner Author

Superseded by land of tip stack #386 (same 12 domain commits).

@YosefHayim YosefHayim closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant