@@ -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
3134describe ( "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} ) ;
0 commit comments