From 75dd7f71bb225c00482561ee255675b8224c6f43 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Mon, 2 Mar 2026 18:29:21 +0100 Subject: [PATCH 1/4] add fromModel helper Signed-off-by: Steven Borrelli --- README.md | 6 +- src/index.ts | 1 + src/resource/resource.test.ts | 128 ++++++++++++++++++++++++++++++++++ src/resource/resource.ts | 42 +++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/resource/resource.test.ts diff --git a/README.md b/README.md index 2746c62..023cee1 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,7 @@ You can use type-safe Kubernetes models from the `kubernetes-models` package: ```typescript import { Deployment } from "kubernetes-models/apps/v1"; import { Pod } from "kubernetes-models/v1"; +import { fromModel } from "@crossplane-org/function-sdk-typescript"; // Create a type-safe Pod const pod = new Pod({ @@ -200,7 +201,10 @@ const pod = new Pod({ // Validate the pod pod.validate(); -// Convert to Resource +// Convert to Resource using the simplified helper +dcds["my-pod"] = fromModel(pod); + +// Alternative: Using the lower-level API dcds["my-pod"] = Resource.fromJSON({ resource: pod.toJSON() }); diff --git a/src/index.ts b/src/index.ts index 1e22898..1b008da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export { asObject, asStruct, fromObject, + fromModel, toObject, newDesiredComposed, mustStructObject, diff --git a/src/resource/resource.test.ts b/src/resource/resource.test.ts new file mode 100644 index 0000000..47cb26f --- /dev/null +++ b/src/resource/resource.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from 'vitest'; +import { fromModel, fromObject, toObject } from './resource.js'; +import { Resource, Ready } from '../proto/run_function.js'; + +describe('fromModel', () => { + it('should convert a kubernetes-models-like object with toJSON() method', () => { + // Simulate a kubernetes-models object + const mockModel = { + metadata: { + name: 'my-pod', + namespace: 'default', + }, + spec: { + containers: [{ name: 'app', image: 'nginx:latest' }], + }, + toJSON() { + return { + apiVersion: 'v1', + kind: 'Pod', + metadata: this.metadata, + spec: this.spec, + }; + }, + }; + + const resource = fromModel(mockModel); + + expect(resource.resource).toEqual({ + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'my-pod', namespace: 'default' }, + spec: { containers: [{ name: 'app', image: 'nginx:latest' }] }, + }); + expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); + expect(resource.connectionDetails).toEqual({}); + }); + + it('should convert a plain JavaScript object', () => { + const plainObject = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'my-config' }, + data: { key: 'value' }, + }; + + const resource = fromModel(plainObject); + + expect(resource.resource).toEqual(plainObject); + expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); + expect(resource.connectionDetails).toEqual({}); + }); + + it('should accept connection details and ready status', () => { + const plainObject = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: 'my-secret' }, + }; + + const connectionDetails = { password: Buffer.from('secret') }; + const ready = Ready.READY_TRUE; + + const resource = fromModel(plainObject, connectionDetails, ready); + + expect(resource.resource).toEqual(plainObject); + expect(resource.ready).toBe(Ready.READY_TRUE); + expect(resource.connectionDetails).toEqual(connectionDetails); + }); + + it('should handle objects with non-function toJSON property', () => { + const objectWithToJSON = { + apiVersion: 'v1', + kind: 'Pod', + toJSON: 'not-a-function', // This should be ignored + }; + + const resource = fromModel(objectWithToJSON); + + // Should use the object as-is since toJSON is not a function + expect(resource.resource).toEqual(objectWithToJSON); + }); +}); + +describe('fromObject', () => { + it('should create a Resource from a plain object', () => { + const obj = { + apiVersion: 'v1', + kind: 'Service', + metadata: { name: 'my-service' }, + }; + + const resource = fromObject(obj); + + expect(resource.resource).toEqual(obj); + expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); + }); +}); + +describe('toObject', () => { + it('should extract the resource object from a Resource', () => { + const obj = { + apiVersion: 'v1', + kind: 'Deployment', + metadata: { name: 'my-deployment' }, + }; + + const resource = Resource.fromJSON({ + resource: obj, + connectionDetails: {}, + ready: Ready.READY_TRUE, + }); + + const extracted = toObject(resource); + + expect(extracted).toEqual(obj); + }); + + it('should return undefined if resource is not present', () => { + const resource = Resource.fromJSON({ + connectionDetails: {}, + ready: Ready.READY_TRUE, + }); + + const extracted = toObject(resource); + + expect(extracted).toBeUndefined(); + }); +}); diff --git a/src/resource/resource.ts b/src/resource/resource.ts index bd10619..11864ae 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -157,3 +157,45 @@ export function fromObject( export function toObject(resource: Resource): Record | undefined { return resource.resource; } + +/** + * Create a Resource from a kubernetes-models object or plain object + * This is a convenience function that accepts objects with a toJSON() method + * (like kubernetes-models) or plain JavaScript objects + * + * @param obj - A kubernetes-models object or plain JavaScript object + * @param connectionDetails - Optional connection details + * @param ready - Optional ready status + * @returns A Resource + * + * @example + * ```typescript + * import { Pod } from "kubernetes-models/v1"; + * + * const pod = new Pod({ + * metadata: { name: "my-pod" }, + * spec: { containers: [{ name: "app", image: "nginx" }] } + * }); + * + * // Simple conversion - automatically calls toJSON() if available + * const resource = fromModel(pod); + * ``` + */ +export function fromModel( + obj: { toJSON: () => Record } | Record, + connectionDetails?: ConnectionDetails, + ready?: Ready +): Resource { + const resourceObj = + typeof obj === 'object' && + obj !== null && + 'toJSON' in obj && + typeof obj.toJSON === 'function' + ? obj.toJSON() + : obj; + return Resource.fromJSON({ + resource: resourceObj as Record, + connectionDetails: connectionDetails || {}, + ready: ready !== undefined ? ready : Ready.READY_UNSPECIFIED, + }); +} From affaca290f77ef8648b98a23136fb860132207b8 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Mon, 2 Mar 2026 19:10:37 +0100 Subject: [PATCH 2/4] update fromModel type signature Signed-off-by: Steven Borrelli --- src/resource/resource.ts | 47 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/resource/resource.ts b/src/resource/resource.ts index 11864ae..e6c301e 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -1,5 +1,5 @@ // Resource utilities for working with Kubernetes resources and protobuf conversion -import { Resource, Ready } from '../proto/run_function.js'; +import { Ready, Resource } from "../proto/run_function.js"; // Type aliases for better readability export type ConnectionDetails = { [key: string]: Buffer }; @@ -51,7 +51,9 @@ export function newDesiredComposed(): DesiredComposed { * @param struct - The protobuf Struct to convert * @returns A plain JavaScript object representing the Kubernetes resource */ -export function asObject(struct: Record | undefined): Record { +export function asObject( + struct: Record | undefined, +): Record { if (!struct) { return {}; } @@ -73,7 +75,9 @@ export function asObject(struct: Record | undefined): Record): Record { +export function asStruct( + obj: Record, +): Record { // In our TypeScript implementation, this is essentially a pass-through // The actual conversion happens in the protobuf serialization layer return obj; @@ -92,12 +96,16 @@ export function asStruct(obj: Record): Record * @returns A Struct representation * @throws Error if conversion fails */ -export function mustStructObject(obj: Record): Record { +export function mustStructObject( + obj: Record, +): Record { try { return asStruct(obj); } catch (error) { throw new Error( - `Failed to convert object to struct: ${error instanceof Error ? error.message : String(error)}` + `Failed to convert object to struct: ${ + error instanceof Error ? error.message : String(error) + }`, ); } } @@ -121,7 +129,9 @@ export function mustStructJSON(json: string): Record { return asStruct(obj); } catch (error) { throw new Error( - `Failed to parse JSON to struct: ${error instanceof Error ? error.message : String(error)}` + `Failed to parse JSON to struct: ${ + error instanceof Error ? error.message : String(error) + }`, ); } } @@ -138,7 +148,7 @@ export function mustStructJSON(json: string): Record { export function fromObject( obj: Record, connectionDetails?: ConnectionDetails, - ready?: Ready + ready?: Ready, ): Resource { return Resource.fromJSON({ resource: obj, @@ -154,7 +164,9 @@ export function fromObject( * @param resource - The Resource to extract from * @returns The plain JavaScript object, or undefined if not present */ -export function toObject(resource: Resource): Record | undefined { +export function toObject( + resource: Resource, +): Record | undefined { return resource.resource; } @@ -181,18 +193,17 @@ export function toObject(resource: Resource): Record | undefine * const resource = fromModel(pod); * ``` */ -export function fromModel( - obj: { toJSON: () => Record } | Record, +export function fromModel>( + obj: T | { toJSON: () => T }, connectionDetails?: ConnectionDetails, - ready?: Ready + ready?: Ready, ): Resource { - const resourceObj = - typeof obj === 'object' && - obj !== null && - 'toJSON' in obj && - typeof obj.toJSON === 'function' - ? obj.toJSON() - : obj; + const resourceObj = typeof obj === "object" && + obj !== null && + "toJSON" in obj && + typeof (obj as any).toJSON === "function" + ? (obj as any).toJSON() + : (obj as T); return Resource.fromJSON({ resource: resourceObj as Record, connectionDetails: connectionDetails || {}, From 494ad75192dc275e1326df82782a86bc673aee9e Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Mon, 2 Mar 2026 20:19:40 +0100 Subject: [PATCH 3/4] refactor based on copilot review Signed-off-by: Steven Borrelli --- src/resource/resource.test.ts | 70 ++++++++++++++++++----------------- src/resource/resource.ts | 30 +++++++-------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/src/resource/resource.test.ts b/src/resource/resource.test.ts index 47cb26f..0932b03 100644 --- a/src/resource/resource.test.ts +++ b/src/resource/resource.test.ts @@ -35,50 +35,33 @@ describe('fromModel', () => { expect(resource.connectionDetails).toEqual({}); }); - it('should convert a plain JavaScript object', () => { - const plainObject = { - apiVersion: 'v1', - kind: 'ConfigMap', - metadata: { name: 'my-config' }, - data: { key: 'value' }, - }; - - const resource = fromModel(plainObject); - - expect(resource.resource).toEqual(plainObject); - expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); - expect(resource.connectionDetails).toEqual({}); - }); - it('should accept connection details and ready status', () => { - const plainObject = { - apiVersion: 'v1', - kind: 'Secret', - metadata: { name: 'my-secret' }, + const mockModel = { + metadata: { + name: 'my-secret', + }, + toJSON() { + return { + apiVersion: 'v1', + kind: 'Secret', + metadata: this.metadata, + }; + }, }; const connectionDetails = { password: Buffer.from('secret') }; const ready = Ready.READY_TRUE; - const resource = fromModel(plainObject, connectionDetails, ready); + const resource = fromModel(mockModel, connectionDetails, ready); - expect(resource.resource).toEqual(plainObject); + expect(resource.resource).toEqual({ + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: 'my-secret' }, + }); expect(resource.ready).toBe(Ready.READY_TRUE); expect(resource.connectionDetails).toEqual(connectionDetails); }); - - it('should handle objects with non-function toJSON property', () => { - const objectWithToJSON = { - apiVersion: 'v1', - kind: 'Pod', - toJSON: 'not-a-function', // This should be ignored - }; - - const resource = fromModel(objectWithToJSON); - - // Should use the object as-is since toJSON is not a function - expect(resource.resource).toEqual(objectWithToJSON); - }); }); describe('fromObject', () => { @@ -93,6 +76,25 @@ describe('fromObject', () => { expect(resource.resource).toEqual(obj); expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); + expect(resource.connectionDetails).toEqual({}); + }); + + it('should accept connection details and ready status', () => { + const obj = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'my-config' }, + data: { key: 'value' }, + }; + + const connectionDetails = { apiKey: Buffer.from('secret') }; + const ready = Ready.READY_TRUE; + + const resource = fromObject(obj, connectionDetails, ready); + + expect(resource.resource).toEqual(obj); + expect(resource.ready).toBe(Ready.READY_TRUE); + expect(resource.connectionDetails).toEqual(connectionDetails); }); }); diff --git a/src/resource/resource.ts b/src/resource/resource.ts index e6c301e..31b327a 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -171,11 +171,13 @@ export function toObject( } /** - * Create a Resource from a kubernetes-models object or plain object - * This is a convenience function that accepts objects with a toJSON() method - * (like kubernetes-models) or plain JavaScript objects + * Create a Resource from a kubernetes-models object + * This is a convenience function for objects with a toJSON() method + * (like kubernetes-models objects) * - * @param obj - A kubernetes-models object or plain JavaScript object + * For plain JavaScript objects, use fromObject() instead. + * + * @param obj - A kubernetes-models object with a toJSON() method * @param connectionDetails - Optional connection details * @param ready - Optional ready status * @returns A Resource @@ -189,24 +191,18 @@ export function toObject( * spec: { containers: [{ name: "app", image: "nginx" }] } * }); * - * // Simple conversion - automatically calls toJSON() if available + * // Automatically calls toJSON() on the model object * const resource = fromModel(pod); * ``` */ export function fromModel>( - obj: T | { toJSON: () => T }, + obj: { toJSON: () => T }, connectionDetails?: ConnectionDetails, ready?: Ready, ): Resource { - const resourceObj = typeof obj === "object" && - obj !== null && - "toJSON" in obj && - typeof (obj as any).toJSON === "function" - ? (obj as any).toJSON() - : (obj as T); - return Resource.fromJSON({ - resource: resourceObj as Record, - connectionDetails: connectionDetails || {}, - ready: ready !== undefined ? ready : Ready.READY_UNSPECIFIED, - }); + return fromObject( + obj.toJSON() as Record, + connectionDetails, + ready, + ); } From 3c4ccc2025e42afa9257e96a00be703596680527 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Mon, 2 Mar 2026 20:30:03 +0100 Subject: [PATCH 4/4] replace double quotes in import Signed-off-by: Steven Borrelli --- src/resource/resource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource/resource.ts b/src/resource/resource.ts index 31b327a..82a2394 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -1,5 +1,5 @@ // Resource utilities for working with Kubernetes resources and protobuf conversion -import { Ready, Resource } from "../proto/run_function.js"; +import { Ready, Resource } from '../proto/run_function.js'; // Type aliases for better readability export type ConnectionDetails = { [key: string]: Buffer };