Skip to content

Commit e35f971

Browse files
committed
Creating metadata object to not expose raw protobuf
1 parent 46b3df5 commit e35f971

2 files changed

Lines changed: 200 additions & 1 deletion

File tree

src/v1.test.ts

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as grpc from "@grpc/grpc-js";
2+
import {} from "@grpc/grpc-js";
23
import { generateTestToken } from "./__utils__/helpers.js";
34
import { Struct } from "./authzedapi/google/protobuf/struct.js";
45
import { PreconnectServices, deadlineInterceptor } from "./util.js";
@@ -18,15 +19,18 @@ import {
1819
LookupSubjectsResponse,
1920
NewClient,
2021
ObjectReference,
22+
PermissionsServiceClient,
2123
Relationship,
2224
RelationshipUpdate,
2325
RelationshipUpdate_Operation,
2426
SubjectReference,
2527
WriteRelationshipsRequest,
2628
WriteRelationshipsResponse,
2729
WriteSchemaRequest,
30+
createStructFromObject,
31+
PbNullValue,
2832
} from "./v1.js";
29-
import { describe, it, expect, beforeEach } from "vitest";
33+
import { describe, it, expect, beforeEach, vi } from "vitest";
3034

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

src/v1.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import {
1717
} from "./util.js";
1818

1919
import type { OmitBaseMethods, PromisifiedClient } from "./types.js";
20+
import { Struct as ImportedPbStruct, NullValue as ImportedPbNullValue } from "./authzedapi/google/protobuf/struct.js";
21+
import type { JsonObject } from "@protobuf-ts/runtime";
22+
23+
export { ImportedPbStruct as PbStruct, ImportedPbNullValue as PbNullValue };
2024

2125
// A merge of the three generated gRPC clients, with their base methods omitted
2226
export type ZedDefaultClientInterface = OmitBaseMethods<
@@ -344,6 +348,20 @@ export function NewClientWithChannelCredentials(
344348
): ZedClientInterface {
345349
return ZedCombinedClient.create(endpoint, creds, preconnect, options);
346350
}
351+
/**
352+
* Creates a google.protobuf.Struct object suitable for use as
353+
* optionalTransactionMetadata in WriteRelationshipsRequest.
354+
*
355+
* @param data A simple JavaScript object (e.g., { key: "value" }) to be converted into a Struct.
356+
* @returns A google.protobuf.Struct object.
357+
*/
358+
export function createStructFromObject(data: JsonObject): ImportedPbStruct {
359+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
360+
// Or handle this case as per library's error handling philosophy
361+
throw new Error('Input data for createStructFromObject must be a non-null object.');
362+
}
363+
return ImportedPbStruct.fromJson(data);
364+
}
347365

348366
export * from "./authzedapi/authzed/api/v1/core.js";
349367
export * from "./authzedapi/authzed/api/v1/experimental_service.js";

0 commit comments

Comments
 (0)