Skip to content

Commit c23f6bc

Browse files
authored
Creating metadata object to not expose raw protobuf (#230)
* Creating metadata object to not expose raw protobuf * prettier * Responding to comments * Formatting changes
1 parent acba4d0 commit c23f6bc

File tree

2 files changed

+250
-1
lines changed

2 files changed

+250
-1
lines changed

src/v1.test.ts

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@ import {
1818
LookupSubjectsResponse,
1919
NewClient,
2020
ObjectReference,
21+
PermissionsServiceClient,
2122
Relationship,
2223
RelationshipUpdate,
2324
RelationshipUpdate_Operation,
2425
SubjectReference,
2526
WriteRelationshipsRequest,
2627
WriteRelationshipsResponse,
2728
WriteSchemaRequest,
29+
createStructFromObject,
30+
PbNullValue,
2831
} from "./v1.js";
29-
import { describe, it, expect, beforeEach } from "vitest";
32+
import { describe, it, expect, beforeEach, vi } from "vitest";
3033

3134
describe("a check with an unknown namespace", () => {
3235
it("should raise a failed precondition", () =>
@@ -681,4 +684,219 @@ describe("Experimental Service", () => {
681684
});
682685
});
683686
}));
687+
688+
describe("WriteRelationships with transaction metadata (Integration Test)", () => {
689+
it("should successfully write relationships with metadata and verify metadata transmission", () =>
690+
new Promise<void>((done, fail) => {
691+
const testToken = generateTestToken("v1-int-tx-metadata");
692+
const client = NewClient(
693+
testToken,
694+
"localhost:50051",
695+
ClientSecurity.INSECURE_LOCALHOST_ALLOWED,
696+
PreconnectServices.SCHEMA_SERVICE |
697+
PreconnectServices.PERMISSIONS_SERVICE,
698+
);
699+
700+
const writeSpy = vi.spyOn(
701+
PermissionsServiceClient.prototype,
702+
"writeRelationships",
703+
);
704+
705+
const schema = `
706+
definition test/user {}
707+
definition test/document {
708+
relation viewer: test/user
709+
permission view = viewer
710+
}
711+
`;
712+
const writeSchemaRequest = WriteSchemaRequest.create({ schema });
713+
714+
client.writeSchema(writeSchemaRequest, (schemaErr, schemaResponse) => {
715+
if (schemaErr) {
716+
client.close();
717+
fail(schemaErr);
718+
return;
719+
}
720+
expect(schemaResponse).toBeDefined();
721+
722+
const uniqueSuffix = Date.now();
723+
const resource = ObjectReference.create({
724+
objectType: "test/document",
725+
objectId: `doc-${uniqueSuffix}`,
726+
});
727+
728+
const user = ObjectReference.create({
729+
objectType: "test/user",
730+
objectId: `user-${uniqueSuffix}`,
731+
});
732+
733+
const updates = [
734+
RelationshipUpdate.create({
735+
relationship: Relationship.create({
736+
resource,
737+
relation: "viewer",
738+
subject: SubjectReference.create({ object: user }),
739+
}),
740+
operation: RelationshipUpdate_Operation.CREATE,
741+
}),
742+
];
743+
744+
const metadataObject = {
745+
transaction_id: "test-tx-123",
746+
other_data: "sample",
747+
};
748+
const transactionMetadata = createStructFromObject(metadataObject);
749+
750+
const writeRequest = WriteRelationshipsRequest.create({
751+
updates,
752+
optionalTransactionMetadata: transactionMetadata,
753+
});
754+
755+
client.writeRelationships(writeRequest, (err, response) => {
756+
if (err) {
757+
client.close();
758+
fail(err);
759+
return;
760+
}
761+
762+
expect(err).toBeNull();
763+
expect(response).toBeDefined();
764+
expect(response?.writtenAt).toBeDefined();
765+
766+
expect(writeSpy).toHaveBeenCalledTimes(1);
767+
768+
const actualRequest = writeSpy.mock
769+
.calls[0][0] as WriteRelationshipsRequest;
770+
771+
expect(actualRequest.updates).toEqual(updates);
772+
773+
expect(actualRequest.optionalTransactionMetadata).toBeDefined();
774+
expect(actualRequest.optionalTransactionMetadata).toEqual(
775+
transactionMetadata,
776+
);
777+
778+
const transactionIdField =
779+
actualRequest.optionalTransactionMetadata?.fields?.[
780+
"transaction_id"
781+
];
782+
expect(transactionIdField?.kind?.oneofKind).toBe("stringValue");
783+
if (transactionIdField?.kind?.oneofKind === "stringValue") {
784+
expect(transactionIdField.kind.stringValue).toBe("test-tx-123");
785+
}
786+
787+
const otherDataField =
788+
actualRequest.optionalTransactionMetadata?.fields?.["other_data"];
789+
expect(otherDataField?.kind?.oneofKind).toBe("stringValue");
790+
if (otherDataField?.kind?.oneofKind === "stringValue") {
791+
expect(otherDataField.kind.stringValue).toBe("sample");
792+
}
793+
794+
client.close();
795+
done();
796+
});
797+
});
798+
}));
799+
});
800+
});
801+
802+
describe("createStructFromObject unit tests", () => {
803+
it("should convert a simple JS object with primitive types", () => {
804+
const obj = {
805+
stringProp: "hello",
806+
numberProp: 123,
807+
booleanProp: true,
808+
};
809+
const struct = createStructFromObject(obj);
810+
expect(struct.fields.stringProp?.kind.oneofKind).toBe("stringValue");
811+
expect(
812+
struct.fields.stringProp?.kind.oneofKind === "stringValue" &&
813+
struct.fields.stringProp?.kind.stringValue,
814+
).toBe("hello");
815+
expect(struct.fields.numberProp?.kind.oneofKind).toBe("numberValue");
816+
expect(
817+
struct.fields.numberProp?.kind.oneofKind === "numberValue" &&
818+
struct.fields.numberProp?.kind.numberValue,
819+
).toBe(123);
820+
expect(struct.fields.booleanProp?.kind.oneofKind).toBe("boolValue");
821+
expect(
822+
struct.fields.booleanProp?.kind.oneofKind === "boolValue" &&
823+
struct.fields.booleanProp?.kind.boolValue,
824+
).toBe(true);
825+
});
826+
827+
it("should convert a JS object with null values", () => {
828+
const obj = {
829+
nullProp: null,
830+
};
831+
const struct = createStructFromObject(obj);
832+
expect(struct.fields.nullProp?.kind.oneofKind).toBe("nullValue");
833+
expect(
834+
struct.fields.nullProp?.kind.oneofKind === "nullValue" &&
835+
struct.fields.nullProp?.kind.nullValue,
836+
).toBe(PbNullValue.NULL_VALUE);
837+
});
838+
839+
it("should convert a JS object with nested objects", () => {
840+
const obj = {
841+
nestedProp: {
842+
innerString: "world",
843+
innerNumber: 456,
844+
},
845+
};
846+
const struct = createStructFromObject(obj);
847+
const nestedStruct =
848+
struct.fields.nestedProp?.kind.oneofKind === "structValue" &&
849+
struct.fields.nestedProp.kind.structValue;
850+
expect(nestedStruct).toBeTruthy();
851+
if (nestedStruct) {
852+
expect(nestedStruct.fields.innerString?.kind.oneofKind).toBe(
853+
"stringValue",
854+
);
855+
expect(
856+
nestedStruct.fields.innerString?.kind.oneofKind === "stringValue" &&
857+
nestedStruct.fields.innerString?.kind.stringValue,
858+
).toBe("world");
859+
expect(nestedStruct.fields.innerNumber?.kind.oneofKind).toBe(
860+
"numberValue",
861+
);
862+
expect(
863+
nestedStruct.fields.innerNumber?.kind.oneofKind === "numberValue" &&
864+
nestedStruct.fields.innerNumber?.kind.numberValue,
865+
).toBe(456);
866+
}
867+
});
868+
869+
it("should convert an empty JS object to an empty Struct", () => {
870+
const obj = {};
871+
const struct = createStructFromObject(obj);
872+
expect(Object.keys(struct.fields).length).toBe(0);
873+
});
874+
875+
it("should throw an error for null input", () => {
876+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
877+
expect(() => createStructFromObject(null as any)).toThrow(
878+
"Input data for createStructFromObject must be a non-null object.",
879+
);
880+
});
881+
882+
it("should throw an error for non-object input (string)", () => {
883+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
884+
expect(() => createStructFromObject("not an object" as any)).toThrow(
885+
"Input data for createStructFromObject must be a non-null object.",
886+
);
887+
});
888+
889+
it("should throw an error for non-object input (number)", () => {
890+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
891+
expect(() => createStructFromObject(123 as any)).toThrow(
892+
"Input data for createStructFromObject must be a non-null object.",
893+
);
894+
});
895+
896+
it("should throw an error for array input", () => {
897+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
898+
expect(() => createStructFromObject([] as any)).toThrow(
899+
"Input data for createStructFromObject must be a non-null object.",
900+
);
901+
});
684902
});

src/v1.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import {
1919
} from "./util.js";
2020

2121
import type { OmitBaseMethods, PromisifiedClient } from "./types.js";
22+
import {
23+
Struct as ImportedPbStruct,
24+
NullValue as ImportedPbNullValue,
25+
} from "./authzedapi/google/protobuf/struct.js";
26+
import type { JsonObject } from "@protobuf-ts/runtime";
27+
28+
export { ImportedPbStruct as PbStruct, ImportedPbNullValue as PbNullValue };
2229

2330
// A merge of the three generated gRPC clients, with their base methods omitted
2431
export type ZedDefaultClientInterface = OmitBaseMethods<
@@ -397,6 +404,30 @@ export function NewClientWithChannelCredentials(
397404
): ZedClientInterface {
398405
return ZedCombinedClient.create(endpoint, creds, preconnect, options);
399406
}
407+
/**
408+
* Creates a google.protobuf.Struct object suitable for use as
409+
* optionalTransactionMetadata in WriteRelationshipsRequest.
410+
*
411+
* @param data A simple JavaScript object (e.g., { key: "value" }) to be converted into a Struct.
412+
* @returns A google.protobuf.Struct object.
413+
*/
414+
export function createStructFromObject(data: JsonObject): ImportedPbStruct {
415+
try {
416+
return ImportedPbStruct.fromJson(data);
417+
} catch (error) {
418+
if (
419+
error instanceof Error &&
420+
error.message.includes(
421+
"Unable to parse message google.protobuf.Struct from JSON",
422+
)
423+
) {
424+
throw new Error(
425+
"Input data for createStructFromObject must be a non-null object.",
426+
);
427+
}
428+
throw error;
429+
}
430+
}
400431

401432
export * from "./authzedapi/authzed/api/v1/core.js";
402433
export * from "./authzedapi/authzed/api/v1/experimental_service.js";

0 commit comments

Comments
 (0)