11import * as grpc from "@grpc/grpc-js" ;
2+ import { } from "@grpc/grpc-js" ;
23import { generateTestToken } from "./__utils__/helpers.js" ;
34import { Struct } from "./authzedapi/google/protobuf/struct.js" ;
45import { 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
3135describe ( "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} ) ;
0 commit comments