Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/main/java/com/chargebee/handlebar/NameFormatHelpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,12 @@ public CharSequence apply(final Object value, final Options options) {
}
return result.toString();
}
},

CONSTANT_CASE {
@Override
public CharSequence apply(final Object value, final Options options) {
return value.toString().toUpperCase().replace("-", "_");
}
}
}
1 change: 1 addition & 0 deletions src/main/java/com/chargebee/sdk/Language.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ private void initialise() throws IOException {
handlebars.registerHelper("pascalCase", NameFormatHelpers.TO_PASCAL);
handlebars.registerHelper(
"operationNameToPascalCase", NameFormatHelpers.OPERATION_NAME_TO_PASCAL_CASE);
handlebars.registerHelper("constantCase", NameFormatHelpers.CONSTANT_CASE);

handlebars.registerHelper(
"snakeCaseToPascalCaseAndSingularize",
Expand Down
45 changes: 40 additions & 5 deletions src/main/java/com/chargebee/sdk/node/NodeV3.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.chargebee.openapi.Spec;
import com.chargebee.sdk.FileOp;
import com.chargebee.sdk.Language;
import com.chargebee.sdk.node.webhook.WebhookGenerator;
import com.github.jknack.handlebars.Template;
import java.io.IOException;
import java.util.*;
Expand All @@ -19,16 +20,47 @@ protected List<FileOp> generateSDK(String outputDirectoryPath, Spec spec) throws
.filter(resource -> !Arrays.stream(this.hiddenOverride).toList().contains(resource.id))
.sorted(Comparator.comparing(Resource::sortOrder))
.toList();

List<FileOp> fileOps = new ArrayList<>();
fileOps.add(generateApiEndpointsFile(outputDirectoryPath, resources));

// Generate webhook event types file
fileOps.addAll(generateWebhookEventTypes(outputDirectoryPath, spec));
// Generate webhook files (content, handler, auth, eventType)
{
Template contentTemplate = getTemplateContent("webhookContent");
Template handlerTemplate = getTemplateContent("webhookHandler");
Template authTemplate = getTemplateContent("webhookAuth");
Template eventTypesTemplate = getTemplateContent("webhookEventTypes");
fileOps.addAll(
WebhookGenerator.generate(
outputDirectoryPath,
spec,
contentTemplate,
handlerTemplate,
authTemplate,
eventTypesTemplate
)
);
}

// Generate entry point files (in parent directory of resources)
String parentDirectoryPath = outputDirectoryPath.replace("/resources", "");
fileOps.addAll(generateEntryPoints(parentDirectoryPath));
{
String parentDirectoryPath = outputDirectoryPath.replace("/resources", "");
Template esmTemplate = getTemplateContent("chargebeeEsm");
Template cjsTemplate = getTemplateContent("chargebeeCjs");
fileOps.add(
new FileOp.WriteString(
parentDirectoryPath,
"chargebee.esm.ts",
esmTemplate.apply("")
)
);
fileOps.add(
new FileOp.WriteString(
parentDirectoryPath,
"chargebee.cjs.ts",
cjsTemplate.apply("")
)
);
}

return fileOps;
}
Expand All @@ -37,6 +69,9 @@ protected List<FileOp> generateSDK(String outputDirectoryPath, Spec spec) throws
protected Map<String, String> templatesDefinition() {
var templates = new HashMap<String, String>();
templates.put("api_endpoints", "/templates/node/api_endpoints.ts.hbs");
templates.put("webhookContent", "/templates/node/webhook_content.ts.hbs");
templates.put("webhookHandler", "/templates/node/webhook_handler.ts.hbs");
templates.put("webhookAuth", "/templates/node/webhook_auth.ts.hbs");
templates.put("webhookEventTypes", "/templates/node/webhook_event_types.ts.hbs");
templates.put("chargebeeEsm", "/templates/node/chargebee_esm.ts.hbs");
templates.put("chargebeeCjs", "/templates/node/chargebee_cjs.ts.hbs");
Expand Down
144 changes: 144 additions & 0 deletions src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.chargebee.sdk.node.webhook;

import com.chargebee.openapi.Attribute;
import com.chargebee.openapi.Resource;
import com.chargebee.openapi.Spec;
import com.chargebee.sdk.FileOp;
import com.github.jknack.handlebars.Template;
import java.io.IOException;
import java.util.*;

public class WebhookGenerator {

private static List<String> getEventResourcesForAEvent(Resource eventResource) {
List<String> resources = new ArrayList<>();
if (eventResource != null) {
for (Attribute attribute : eventResource.attributes()) {
if (attribute.name.equals("content")) {
attribute
.attributes()
.forEach(
(innerAttribute -> {
String ref = innerAttribute.schema.get$ref();
if (ref != null && ref.contains("/")) {
String schemaName = ref.substring(ref.lastIndexOf("/") + 1);
resources.add(schemaName);
}
}));
}
}
}
return resources;
}

public static List<FileOp> generate(
String outputDirectoryPath,
Spec spec,
Template contentTemplate,
Template handlerTemplate,
Template authTemplate,
Template eventTypesTemplate)
throws IOException {
final String webhookDirectoryPath = "/webhook";
List<FileOp> fileOps = new ArrayList<>();
// Ensure webhook directory exists
fileOps.add(new FileOp.CreateDirectory(outputDirectoryPath, webhookDirectoryPath));

// Include deprecated webhook events (like PCV1) since customers may still receive them
var webhookInfo = spec.extractWebhookInfo(true);
var eventSchema = spec.resourcesForEvents();

if (webhookInfo.isEmpty()) {
return fileOps;
}

List<Map<String, Object>> events = new ArrayList<>();
Set<String> seenTypes = new HashSet<>();
Set<String> uniqueImports = new HashSet<>();

for (Map<String, String> info : webhookInfo) {
String type = info.get("type");
if (seenTypes.contains(type)) {
continue;
}
seenTypes.add(type);

String resourceSchemaName = info.get("resource_schema_name");
Resource matchedSchema =
eventSchema.stream()
.filter(schema -> schema.name.equals(resourceSchemaName))
.findFirst()
.orElse(null);

List<String> allSchemas = getEventResourcesForAEvent(matchedSchema);
List<String> schemaImports = new ArrayList<>();

for(String schema : allSchemas) {
schemaImports.add(schema);
uniqueImports.add(schema);
}

Map<String, Object> params = new HashMap<>();
params.put("type", type);
params.put("resource_schemas", schemaImports);
events.add(params);
}

events.sort(Comparator.comparing(e -> e.get("type").toString()));

// content.ts
{
Map<String, Object> ctx = new HashMap<>();
ctx.put("events", events);
List<String> importsList = new ArrayList<>(uniqueImports);
Collections.sort(importsList);
ctx.put("unique_imports", importsList);

fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath,
"content.ts",
contentTemplate.apply(ctx)
)
);
}

// handler.ts (static template)
{
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath,
"handler.ts",
handlerTemplate.apply("")
)
);
}

// auth.ts
{
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath,
"auth.ts",
authTemplate.apply("")
)
);
}

// eventType.ts
{
Map<String, Object> ctx = new HashMap<>();
ctx.put("events", events);
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath,
"eventType.ts",
eventTypesTemplate.apply(ctx)
)
);
}

return fileOps;
}
}

11 changes: 10 additions & 1 deletion src/main/resources/templates/node/chargebee_cjs.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { CreateChargebee } from './createChargebee.js';
import { FetchHttpClient } from './net/FetchClient.js';
import { WebhookEventType, WebhookContentType } from './resources/webhook/eventType.js';
import { WebhookHandler, WebhookEventType, WebhookContentType } from './resources/webhook/handler.js';
import webhookInstance from './resources/webhook/handler.js';
import { basicAuthValidator } from './resources/webhook/auth.js';

const httpClient = new FetchHttpClient();
const Chargebee = CreateChargebee(httpClient);
module.exports = Chargebee;
module.exports.Chargebee = Chargebee;
module.exports.default = Chargebee;

// Export webhook modules
module.exports.WebhookHandler = WebhookHandler;
// Export webhook event types
module.exports.WebhookEventType = WebhookEventType;
module.exports.WebhookContentType = WebhookContentType;
module.exports.webhook = webhookInstance;
module.exports.basicAuthValidator = basicAuthValidator;

// Export webhook types
export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, RequestValidator } from './resources/webhook/handler.js';
export type { CredentialValidator } from './resources/webhook/auth.js';
9 changes: 7 additions & 2 deletions src/main/resources/templates/node/chargebee_esm.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const Chargebee = CreateChargebee(httpClient);

export default Chargebee;

// Export webhook event types
export { WebhookEventType, WebhookContentType } from './resources/webhook/eventType.js';
// Export webhook modules
export { WebhookHandler, WebhookEventType, WebhookContentType } from './resources/webhook/handler.js';
export { default as webhook } from './resources/webhook/handler.js';
export { basicAuthValidator } from './resources/webhook/auth.js';

// Export webhook types
export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, RequestValidator } from './resources/webhook/handler.js';
export type { CredentialValidator } from './resources/webhook/auth.js';
64 changes: 64 additions & 0 deletions src/main/resources/templates/node/webhook_auth.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Credential validator function type.
* Can be synchronous or asynchronous (e.g., for database lookups).
*/
export type CredentialValidator = (
username: string,
password: string,
) => boolean | Promise<boolean>;

/**
* Creates a Basic Auth validator for webhook requests.
* Parses the Authorization header and validates credentials.
*
* @param validateCredentials - Function to validate username/password.
* Can be sync or async (e.g., for database lookups).
* @returns A request validator function for use with WebhookHandler
*
* @example
* // Simple sync validation
* const validator = basicAuthValidator((u, p) => u === 'admin' && p === 'secret');
*
* @example
* // Async validation (e.g., database lookup)
* const validator = basicAuthValidator(async (u, p) => {
* const user = await db.findUser(u);
* return user && await bcrypt.compare(p, user.passwordHash);
* });
*/
export const basicAuthValidator = (
validateCredentials: CredentialValidator,
) => {
return async (headers: Record<string, string | string[] | undefined>): Promise<void> => {
const authHeader = headers['authorization'] || headers['Authorization'];

if (!authHeader) {
throw new Error('Missing authorization header');
}

const authStr = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (!authStr) {
throw new Error('Invalid authorization header');
}

const parts = authStr.split(' ');
if (parts.length !== 2 || parts[0] !== 'Basic') {
throw new Error('Invalid authorization header format');
}

const decoded = Buffer.from(parts[1], 'base64').toString();
const separatorIndex = decoded.indexOf(':');

if (separatorIndex === -1) {
throw new Error('Invalid credentials format');
}

const username = decoded.substring(0, separatorIndex);
const password = decoded.substring(separatorIndex + 1);

const isValid = await validateCredentials(username, password);
if (!isValid) {
throw new Error('Invalid credentials');
}
};
};
22 changes: 22 additions & 0 deletions src/main/resources/templates/node/webhook_content.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
///<reference path='../../../types/index.d.ts'/>
{{#each events}}

export interface {{snakeCaseToPascalCase type}}Content {
{{#each resource_schemas}}
{{this}}: import('chargebee').{{this}};
{{/each}}
}
{{/each}}

export interface WebhookEvent {
id: string;
occurred_at: number;
source: string;
user?: string;
webhook_status: string;
webhook_failure_reason?: string;
webhooks?: any[];
event_type: string;
api_version: string;
content: any;
}
Loading