Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions scripts/fix-generated-types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,23 @@ content = content.replace(/export enum [^{]+\{[\s\S]*?\}/g, (block) =>
block.replace(/= '([^']+)'/g, (_, value) => `= '${toConstantCase(value)}'`)
);

content = content.replace(
/export interface RequestPurchaseProps \{[\s\S]*?\}\n\n/,
[
'export type RequestPurchaseProps =',
' | {',
' /** Per-platform purchase request props */',
' request: RequestPurchasePropsByPlatforms;',
" type: 'in-app';",
' }',
' | {',
' /** Per-platform subscription request props */',
' request: RequestSubscriptionPropsByPlatforms;',
" type: 'subs';",
' };\n\n',
].join('\n'),
);

const futureFields = new Set();
for (const file of schemaFiles) {
let previousWasMarker = false;
Expand Down
135 changes: 133 additions & 2 deletions scripts/generate-dart-types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,91 @@ const printObject = (objectType) => {
};

const printInput = (inputType) => {
if (inputType.name === 'RequestPurchaseProps') {
addDocComment(lines, inputType.description);
lines.push('class RequestPurchaseProps {');
lines.push(' RequestPurchaseProps({');
lines.push(' required this.request,');
lines.push(' ProductQueryType? type,');
lines.push(' }) : type = type ?? (request is RequestPurchasePropsRequestPurchase');
lines.push(' ? ProductQueryType.InApp');
lines.push(' : ProductQueryType.Subs) {');
lines.push(' if (request is RequestPurchasePropsRequestPurchase && this.type != ProductQueryType.InApp) {');
lines.push(" throw ArgumentError('type must be IN_APP when requestPurchase is provided');");
lines.push(' }');
lines.push(' if (request is RequestPurchasePropsRequestSubscription && this.type != ProductQueryType.Subs) {');
lines.push(" throw ArgumentError('type must be SUBS when requestSubscription is provided');");
lines.push(' }');
lines.push(' }');
lines.push('');
lines.push(' final RequestPurchasePropsRequest request;');
lines.push(' final ProductQueryType type;');
lines.push('');
lines.push(' factory RequestPurchaseProps.fromJson(Map<String, dynamic> json) {');
lines.push(" final typeValue = json['type'] as String?;");
lines.push(' final parsedType = typeValue != null ? ProductQueryType.fromJson(typeValue) : null;');
lines.push(" final purchaseJson = json['requestPurchase'] as Map<String, dynamic>?;");
lines.push(' if (purchaseJson != null) {');
lines.push(' final request = RequestPurchasePropsRequestPurchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson));');
lines.push(' final finalType = parsedType ?? ProductQueryType.InApp;');
lines.push(' if (finalType != ProductQueryType.InApp) {');
lines.push(" throw ArgumentError('type must be IN_APP when requestPurchase is provided');");
lines.push(' }');
lines.push(' return RequestPurchaseProps(request: request, type: finalType);');
lines.push(' }');
lines.push(" final subscriptionJson = json['requestSubscription'] as Map<String, dynamic>?;");
lines.push(' if (subscriptionJson != null) {');
lines.push(' final request = RequestPurchasePropsRequestSubscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson));');
lines.push(' final finalType = parsedType ?? ProductQueryType.Subs;');
lines.push(' if (finalType != ProductQueryType.Subs) {');
lines.push(" throw ArgumentError('type must be SUBS when requestSubscription is provided');");
lines.push(' }');
lines.push(' return RequestPurchaseProps(request: request, type: finalType);');
lines.push(' }');
lines.push(" throw ArgumentError('RequestPurchaseProps requires requestPurchase or requestSubscription');");
lines.push(' }');
lines.push('');
lines.push(' Map<String, dynamic> toJson() {');
lines.push(' if (request is RequestPurchasePropsRequestPurchase) {');
lines.push(' return {');
lines.push(" 'requestPurchase': (request as RequestPurchasePropsRequestPurchase).value.toJson(),");
lines.push(" 'type': type.toJson(),");
lines.push(' };');
lines.push(' }');
lines.push(' if (request is RequestPurchasePropsRequestSubscription) {');
lines.push(' return {');
lines.push(" 'requestSubscription': (request as RequestPurchasePropsRequestSubscription).value.toJson(),");
lines.push(" 'type': type.toJson(),");
lines.push(' };');
lines.push(' }');
lines.push(" throw StateError('Unsupported RequestPurchaseProps request variant');");
lines.push(' }');
lines.push('');
lines.push(' static RequestPurchaseProps inApp({required RequestPurchasePropsByPlatforms request}) {');
lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestPurchase(request), type: ProductQueryType.InApp);');
lines.push(' }');
lines.push('');
lines.push(' static RequestPurchaseProps subs({required RequestSubscriptionPropsByPlatforms request}) {');
lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestSubscription(request), type: ProductQueryType.Subs);');
lines.push(' }');
lines.push('}');
lines.push('');
lines.push('sealed class RequestPurchasePropsRequest {');
lines.push(' const RequestPurchasePropsRequest();');
lines.push('}');
lines.push('');
lines.push('class RequestPurchasePropsRequestPurchase extends RequestPurchasePropsRequest {');
lines.push(' const RequestPurchasePropsRequestPurchase(this.value);');
lines.push(' final RequestPurchasePropsByPlatforms value;');
lines.push('}');
lines.push('');
lines.push('class RequestPurchasePropsRequestSubscription extends RequestPurchasePropsRequest {');
lines.push(' const RequestPurchasePropsRequestSubscription(this.value);');
lines.push(' final RequestSubscriptionPropsByPlatforms value;');
lines.push('}');
lines.push('');
return;
}
addDocComment(lines, inputType.description);
lines.push(`class ${inputType.name} {`);
lines.push(` const ${inputType.name}({`);
Expand Down Expand Up @@ -432,8 +517,27 @@ const printInput = (inputType) => {

const printUnion = (unionType) => {
addDocComment(lines, unionType.description);
const members = unionType.getTypes().map((member) => member.name).sort();
lines.push(`sealed class ${unionType.name} {`);
const memberTypes = unionType.getTypes();
const members = memberTypes.map((member) => member.name).sort();

let sharedInterfaceNames = [];
if (memberTypes.length > 0) {
const [firstMember, ...otherMembers] = memberTypes;
const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name));
for (const member of otherMembers) {
const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name));
for (const ifaceName of Array.from(firstInterfaces)) {
if (!memberInterfaces.has(ifaceName)) {
firstInterfaces.delete(ifaceName);
}
}
}
sharedInterfaceNames = Array.from(firstInterfaces).sort();
}

const implementsClause = sharedInterfaceNames.length ? ` implements ${sharedInterfaceNames.join(', ')}` : '';

lines.push(`sealed class ${unionType.name}${implementsClause} {`);
lines.push(` const ${unionType.name}();`, '');
lines.push(` factory ${unionType.name}.fromJson(Map<String, dynamic> json) {`);
lines.push(` final typeName = json['__typename'] as String?;`);
Expand All @@ -444,6 +548,33 @@ const printUnion = (unionType) => {
lines.push(' }');
lines.push(` throw ArgumentError('Unknown __typename for ${unionType.name}: $typeName');`);
lines.push(' }', '');

if (sharedInterfaceNames.length) {
const interfaceFieldMap = new Map();
for (const interfaceName of sharedInterfaceNames) {
const interfaceType = schema.getType(interfaceName);
if (!interfaceType || !isInterfaceType(interfaceType)) continue;
const fields = Object.values(interfaceType.getFields()).sort((a, b) => a.name.localeCompare(b.name));
for (const field of fields) {
if (interfaceFieldMap.has(field.name)) continue;
const { type, nullable } = getDartType(field.type);
interfaceFieldMap.set(field.name, { field, type, nullable });
}
}

const interfaceFields = Array.from(interfaceFieldMap.values()).sort((a, b) => a.field.name.localeCompare(b.field.name));
interfaceFields.forEach(({ field, type, nullable }) => {
addDocComment(lines, field.description, ' ');
const fieldType = `${type}${nullable ? '?' : ''}`;
const fieldName = escapeDartName(field.name);
lines.push(' @override');
lines.push(` ${fieldType} get ${fieldName};`);
});
if (interfaceFields.length) {
lines.push('');
}
}

lines.push(' Map<String, dynamic> toJson();');
lines.push('}', '');
};
Expand Down
77 changes: 74 additions & 3 deletions scripts/generate-kotlin-types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,61 @@ const printDataClass = (objectType) => {
};

const printInput = (inputType) => {
if (inputType.name === 'RequestPurchaseProps') {
addDocComment(lines, inputType.description);
lines.push('public data class RequestPurchaseProps(');
lines.push(' val request: Request,');
lines.push(' val type: ProductQueryType');
lines.push(') {');
lines.push(' init {');
lines.push(' when (request) {');
lines.push(' is Request.Purchase -> require(type == ProductQueryType.InApp) { "type must be IN_APP when request is purchase" }');
lines.push(' is Request.Subscription -> require(type == ProductQueryType.Subs) { "type must be SUBS when request is subscription" }');
lines.push(' }');
lines.push(' }');
lines.push('');
lines.push(' companion object {');
lines.push(' fun fromJson(json: Map<String, Any?>): RequestPurchaseProps {');
lines.push(' val rawType = (json["type"] as String?)?.let { ProductQueryType.fromJson(it) }');
lines.push(' val purchaseJson = json["requestPurchase"] as Map<String, Any?>?');
lines.push(' if (purchaseJson != null) {');
lines.push(' val request = Request.Purchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson))');
lines.push(' val finalType = rawType ?: ProductQueryType.InApp');
lines.push(' require(finalType == ProductQueryType.InApp) { "type must be IN_APP when requestPurchase is provided" }');
lines.push(' return RequestPurchaseProps(request = request, type = finalType)');
lines.push(' }');
lines.push(' val subscriptionJson = json["requestSubscription"] as Map<String, Any?>?');
lines.push(' if (subscriptionJson != null) {');
lines.push(' val request = Request.Subscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson))');
lines.push(' val finalType = rawType ?: ProductQueryType.Subs');
lines.push(' require(finalType == ProductQueryType.Subs) { "type must be SUBS when requestSubscription is provided" }');
lines.push(' return RequestPurchaseProps(request = request, type = finalType)');
lines.push(' }');
lines.push(' throw IllegalArgumentException("RequestPurchaseProps requires requestPurchase or requestSubscription")');
lines.push(' }');
lines.push(' }');
lines.push('');
lines.push(' fun toJson(): Map<String, Any?> = when (request) {');
lines.push(' is Request.Purchase -> mapOf(');
lines.push(' "requestPurchase" to request.value.toJson(),');
lines.push(' "type" to type.toJson(),');
lines.push(' )');
lines.push(' is Request.Subscription -> mapOf(');
lines.push(' "requestSubscription" to request.value.toJson(),');
lines.push(' "type" to type.toJson(),');
lines.push(' )');
lines.push(' }');
lines.push('');
lines.push(' sealed class Request {');
lines.push(' data class Purchase(val value: RequestPurchasePropsByPlatforms) : Request()');
lines.push(' data class Subscription(val value: RequestSubscriptionPropsByPlatforms) : Request()');
lines.push(' }');
lines.push('}');
lines.push('');
return;
}
addDocComment(lines, inputType.description);
lines.push(`public data class ${inputType.name}(`);
const fields = Object.values(inputType.getFields()).sort((a, b) => a.name.localeCompare(b.name));
const fieldInfos = fields.map((field) => {
const { type, nullable, metadata } = getKotlinType(field.type);
Expand All @@ -399,7 +453,6 @@ const printInput = (inputType) => {
const defaultValue = nullable ? ' = null' : '';
return { field, propertyName, propertyType, defaultValue, metadata };
});
lines.push(`public data class ${inputType.name}(`);
fieldInfos.forEach(({ field, propertyName, propertyType, defaultValue }, index) => {
addDocComment(lines, field.description, ' ');
const suffix = index === fieldInfos.length - 1 ? '' : ',';
Expand Down Expand Up @@ -427,8 +480,26 @@ const printInput = (inputType) => {

const printUnion = (unionType) => {
addDocComment(lines, unionType.description);
const members = unionType.getTypes().map((member) => member.name).sort();
lines.push(`public sealed interface ${unionType.name} {`);
const memberTypes = unionType.getTypes();
const members = memberTypes.map((member) => member.name).sort();

let sharedInterfaceNames = [];
if (memberTypes.length > 0) {
const [firstMember, ...otherMembers] = memberTypes;
const firstInterfaces = new Set(firstMember.getInterfaces().map((iface) => iface.name));
for (const member of otherMembers) {
const memberInterfaces = new Set(member.getInterfaces().map((iface) => iface.name));
for (const ifaceName of Array.from(firstInterfaces)) {
if (!memberInterfaces.has(ifaceName)) {
firstInterfaces.delete(ifaceName);
}
}
}
sharedInterfaceNames = Array.from(firstInterfaces).sort();
}

const implementations = sharedInterfaceNames.length ? ` : ${sharedInterfaceNames.join(', ')}` : '';
lines.push(`public sealed interface ${unionType.name}${implementations} {`);
lines.push(' fun toJson(): Map<String, Any?>', '');
lines.push(' companion object {');
lines.push(` fun fromJson(json: Map<String, Any?>): ${unionType.name} {`);
Expand Down
Loading