From 99d698201b5937c84aee4dfc8074a632116dc31b Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 21 Jan 2025 14:06:45 -0800 Subject: [PATCH 01/10] feat: backend tenant graphql resolvers --- .../generated/graphql.ts | 193 +++- .../src/graphql/generated/graphql.schema.json | 828 ++++++++++++++++-- .../backend/src/graphql/generated/graphql.ts | 193 +++- .../backend/src/graphql/resolvers/index.ts | 18 +- .../src/graphql/resolvers/tenant.test.ts | 508 +++++++++++ .../backend/src/graphql/resolvers/tenant.ts | 179 ++++ packages/backend/src/graphql/schema.graphql | 103 +++ packages/backend/src/tests/app.ts | 1 + packages/backend/src/tests/tenant.ts | 10 + packages/frontend/app/generated/graphql.ts | 193 +++- .../src/generated/graphql.ts | 193 +++- test/integration/lib/generated/graphql.ts | 193 +++- 12 files changed, 2550 insertions(+), 62 deletions(-) create mode 100644 packages/backend/src/graphql/resolvers/tenant.test.ts create mode 100644 packages/backend/src/graphql/resolvers/tenant.ts diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 1745966aa2..c745f24e15 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2105,6 +2105,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "apiSecret", + "description": "Secret used to secure requests made for this tenant.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Contact email of the tenant owner.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentUrl", + "description": "URL of the tenant's identity provider's consent screen.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "Secret used to secure requests from the tenant's identity provider.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicName", + "description": "Public name for the tenant.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateWalletAddressInput", @@ -2513,6 +2600,33 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "DeleteTenantMutationResponse", + "description": null, + "fields": [ + { + "name": "success", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "DepositAssetLiquidityInput", @@ -3988,6 +4102,11 @@ "name": "Peer", "ofType": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + }, { "kind": "OBJECT", "name": "WalletAddress", @@ -4489,6 +4608,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createTenant", + "description": "Create a tenant.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateTenantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createWalletAddress", "description": "Create a new wallet address.", @@ -4646,6 +4798,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "deleteTenant", + "description": "Delete a tenant.", + "args": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DeleteTenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "depositAssetLiquidity", "description": "Deposit asset liquidity.", @@ -4985,6 +5170,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "updateTenant", + "description": "Update a tenant.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateTenantInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantMutationResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updateWalletAddress", "description": "Update an existing wallet address.", @@ -6811,41 +7029,12 @@ "deprecationReason": null }, { - "name": "walletAddress", - "description": "Fetch a wallet address by its ID.", + "name": "tenant", + "description": "Retrieve a tenant of the instance.", "args": [ { "name": "id", - "description": "Unique identifier of the wallet address.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "OBJECT", - "name": "WalletAddress", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "walletAddressByUrl", - "description": "Get a wallet address by its url if it exists", - "args": [ - { - "name": "url", - "description": "Wallet Address URL.", + "description": "Unique identifier of the tenant.", "type": { "kind": "NON_NULL", "name": null, @@ -6862,19 +7051,19 @@ ], "type": { "kind": "OBJECT", - "name": "WalletAddress", + "name": "Tenant", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "walletAddresses", - "description": "Fetch a paginated list of wallet addresses.", + "name": "tenants", + "description": "Fetch a paginated list of tenants on the instance.", "args": [ { "name": "after", - "description": "Forward pagination: Cursor (wallet address ID) to start retrieving wallet addresses after this point.", + "description": "Forward pagination: Cursor (tenant ID) to start retrieving tenants after this point.", "type": { "kind": "SCALAR", "name": "String", @@ -6886,7 +7075,7 @@ }, { "name": "before", - "description": "Backward pagination: Cursor (wallet address ID) to start retrieving wallet addresses before this point.", + "description": "Backward pagination: Cursor (tenant ID) to start retrieving tenants before this point.", "type": { "kind": "SCALAR", "name": "String", @@ -6898,7 +7087,7 @@ }, { "name": "first", - "description": "Forward pagination: Limit the result to the first **n** wallet addresses after the `after` cursor.", + "description": "Forward pagination: Limit the result to the first **n** tenants after the `after` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -6910,7 +7099,7 @@ }, { "name": "last", - "description": "Backward pagination: Limit the result to the last **n** wallet addresses before the `before` cursor.", + "description": "Backward pagination: Limit the result to the last **n** tenants before the `before` cursor.", "type": { "kind": "SCALAR", "name": "Int", @@ -6922,7 +7111,7 @@ }, { "name": "sortOrder", - "description": "Specify the sort order of wallet addresses based on their creation date, either ascending or descending.", + "description": "Specify the sort order of tenants based on their creation date, either ascending or descending.", "type": { "kind": "ENUM", "name": "SortOrder", @@ -6938,7 +7127,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "WalletAddressesConnection", + "name": "TenantsConnection", "ofType": null } }, @@ -6946,22 +7135,157 @@ "deprecationReason": null }, { - "name": "webhookEvents", - "description": "Fetch a paginated list of webhook events.", + "name": "walletAddress", + "description": "Fetch a wallet address by its ID.", "args": [ { - "name": "after", - "description": "Forward pagination: Cursor (webhook event ID) to start retrieving webhook events after this point.", + "name": "id", + "description": "Unique identifier of the wallet address.", "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, - { + } + ], + "type": { + "kind": "OBJECT", + "name": "WalletAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "walletAddressByUrl", + "description": "Get a wallet address by its url if it exists", + "args": [ + { + "name": "url", + "description": "Wallet Address URL.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "WalletAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "walletAddresses", + "description": "Fetch a paginated list of wallet addresses.", + "args": [ + { + "name": "after", + "description": "Forward pagination: Cursor (wallet address ID) to start retrieving wallet addresses after this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Backward pagination: Cursor (wallet address ID) to start retrieving wallet addresses before this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Forward pagination: Limit the result to the first **n** wallet addresses after the `after` cursor.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Backward pagination: Limit the result to the last **n** wallet addresses before the `before` cursor.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Specify the sort order of wallet addresses based on their creation date, either ascending or descending.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WalletAddressesConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webhookEvents", + "description": "Fetch a paginated list of webhook events.", + "args": [ + { + "name": "after", + "description": "Forward pagination: Cursor (webhook event ID) to start retrieving webhook events after this point.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "before", "description": "Backward pagination: Cursor (webhook event ID) to start retrieving webhook events before this point.", "type": { @@ -7033,6 +7357,22 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "whoami", + "description": "Determine if the requester has operator permissions", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WhoamiResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -7624,6 +7964,264 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Tenant", + "description": null, + "fields": [ + { + "name": "apiSecret", + "description": "Secret used to secure requests made for this tenant.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "The date and time that this tenant was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletedAt", + "description": "The date and time that this tenant was deleted.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Contact email of the tenant owner.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Unique identifier of the tenant.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentUrl", + "description": "URL of the tenant's identity provider's consent screen.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "Secret used to secure requests from the tenant's identity provider.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicName", + "description": "Public name for the tenant.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Model", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantEdge", + "description": null, + "fields": [ + { + "name": "cursor", + "description": "A cursor for paginating through the tenants.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "A tenant node in the list.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantMutationResponse", + "description": null, + "fields": [ + { + "name": "tenant", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TenantsConnection", + "description": null, + "fields": [ + { + "name": "edges", + "description": "A list of edges representing tenants and cursors for pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TenantEdge", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "TransferState", @@ -7992,6 +8590,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateTenantInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "apiSecret", + "description": "Secret used to secure requests made for this tenant.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Contact email of the tenant owner.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Unique identifier of the tenant.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpConsentUrl", + "description": "URL of the tenant's identity provider's consent screen.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idpSecret", + "description": "Secret used to secure requests from the tenant's identity provider.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "publicName", + "description": "Public name for the tenant.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "UpdateWalletAddressInput", @@ -9158,6 +9843,49 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "WhoamiResponse", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isOperator", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "WithdrawEventLiquidityInput", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index 2c191b30e8..343c07a475 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -77,6 +77,14 @@ import { GraphQLJSONObject } from 'graphql-scalars' import { getCombinedPayments } from './combined_payments' import { createOrUpdatePeerByUrl } from './auto-peering' import { getAccountingTransfers } from './accounting_transfer' +import { + whoami, + createTenant, + updateTenant, + deleteTenant, + getTenant, + getTenants +} from './tenant' export const resolvers: Resolvers = { UInt8: GraphQLUInt8, @@ -92,6 +100,7 @@ export const resolvers: Resolvers = { liquidity: getPeerLiquidity }, Query: { + whoami, walletAddress: getWalletAddress, walletAddressByUrl: getWalletAddressByUrl, walletAddresses: getWalletAddresses, @@ -108,7 +117,9 @@ export const resolvers: Resolvers = { webhookEvents: getWebhookEvents, payments: getCombinedPayments, accountingTransfers: getAccountingTransfers, - receiver: getReceiver + receiver: getReceiver, + tenant: getTenant, + tenants: getTenants }, WalletAddress: { liquidity: getWalletAddressLiquidity, @@ -161,6 +172,9 @@ export const resolvers: Resolvers = { createIncomingPaymentWithdrawal, createOutgoingPaymentWithdrawal, setFee, - updateIncomingPayment + updateIncomingPayment, + createTenant, + updateTenant, + deleteTenant } } diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts new file mode 100644 index 0000000000..4882d8b7bd --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -0,0 +1,508 @@ +import { IocContract } from '@adonisjs/fold' +import { AppServices } from '../../app' +import { createTestApp, TestContainer } from '../../tests/app' +import { + DeleteTenantMutationResponse, + Tenant, + TenantMutationResponse, + TenantsConnection, + WhoamiResponse +} from '../generated/graphql' +import { initIocContainer } from '../..' +import { Config, IAppConfig } from '../../config/app' +import { createTenant, generateTenantInput } from '../../tests/tenant' +import { ApolloError, gql, NormalizedCacheObject } from '@apollo/client' +import { getPageTests } from './page.test' +import { truncateTables } from '../../tests/tableManager' +import nock from 'nock' +import { + createHttpLink, + ApolloLink, + ApolloClient, + InMemoryCache +} from '@apollo/client' +import { setContext } from '@apollo/client/link/context' +import { GraphQLErrorCode } from '../errors' +import { Tenant as TenantModel } from '../../tenants/model' + +function createTenantedApolloClient( + appContainer: TestContainer, + tenantId: string +): ApolloClient { + const httpLink = createHttpLink({ + uri: `http://localhost:${appContainer.app.getAdminPort()}/graphql`, + fetch + }) + const authLink = setContext((_, { headers }) => { + return { + headers: { + ...headers, + 'tenant-id': tenantId + } + } + }) + + const link = ApolloLink.from([authLink, httpLink]) + + return new ApolloClient({ + cache: new InMemoryCache({}), + link: link, + defaultOptions: { + query: { + fetchPolicy: 'no-cache' + }, + mutate: { + fetchPolicy: 'no-cache' + }, + watchQuery: { + fetchPolicy: 'no-cache' + } + } + }) +} + +describe('Tenant Resolvers', (): void => { + let deps: IocContract + let appContainer: TestContainer + let config: IAppConfig + + beforeAll(async (): Promise => { + deps = await initIocContainer(Config) + appContainer = await createTestApp(deps) + config = await deps.use('config') + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + afterAll(async (): Promise => { + await appContainer.apolloClient.stop() + await appContainer.shutdown() + }) + + describe('whoami', (): void => { + test.each` + isOperator | description + ${true} | ${'operator'} + ${false} | ${'tenant'} + `('whoami query as $description', async ({ isOperator }): Promise => { + const tenant = await createTenant(deps) + const client = isOperator + ? appContainer.apolloClient + : createTenantedApolloClient(appContainer, tenant.id) + + const result = await client + .query({ + query: gql` + query Whoami { + whoami { + id + isOperator + } + } + ` + }) + .then((query): WhoamiResponse => query.data?.whoami) + + expect(result).toEqual({ + id: isOperator ? config.operatorTenantId : tenant.id, + isOperator, + __typename: 'WhoamiResponse' + }) + }) + }) + + describe('Query.tenant', (): void => { + describe('page tests', (): void => { + getPageTests({ + getClient: () => appContainer.apolloClient, + createModel: () => createTenant(deps), + pagedQuery: 'tenants' + }) + + test('Cannot get page as non-operator', async (): Promise => { + const tenant = await createTenant(deps) + const apolloClient = createTenantedApolloClient(appContainer, tenant.id) + try { + await apolloClient + .query({ + query: gql` + query GetTenants { + tenants { + edges { + node { + id + } + } + } + } + ` + }) + .then((query): TenantsConnection => query.data?.tenants) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'cannot get tenants page', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.Forbidden + }) + }) + ) + } + }) + }) + + test('can get tenant as operator', async (): Promise => { + const tenant = await createTenant(deps) + + const query = await appContainer.apolloClient + .query({ + query: gql` + query Tenant($id: String!) { + tenant(id: $id) { + id + email + } + } + `, + variables: { + id: tenant.id + } + }) + .then((query): Tenant => query.data?.tenant) + + expect(query).toEqual({ + id: tenant.id, + email: tenant.email, + __typename: 'Tenant' + }) + }) + + test('can get own tenant', async (): Promise => { + const tenant = await createTenant(deps) + const apolloClient = createTenantedApolloClient(appContainer, tenant.id) + + const query = await apolloClient + .query({ + query: gql` + query Tenant($id: String!) { + tenant(id: $id) { + id + email + } + } + `, + variables: { + id: tenant.id + } + }) + .then((query): Tenant => query.data?.tenant) + + expect(query).toEqual({ + id: tenant.id, + email: tenant.email, + __typename: 'Tenant' + }) + }) + + test('cannot get other tenant as non-operator', async (): Promise => { + const firstTenant = await createTenant(deps) + const secondTenant = await createTenant(deps) + + const apolloClient = createTenantedApolloClient( + appContainer, + firstTenant.id + ) + + try { + await apolloClient + .query({ + query: gql` + query Tenant($id: String!) { + tenant(id: $id) { + id + email + } + } + `, + variables: { + id: secondTenant.id + } + }) + .then((query): Tenant => query.data?.tenant) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'tenant does not exist', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) + }) + + describe('Mutations', (): void => { + describe('Create', (): void => { + test('can create a tenant', async (): Promise => { + const input = generateTenantInput() + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { createTenant: { id: 1234 } } }) + + const mutation = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input + } + }) + .then((query): TenantMutationResponse => query.data?.createTenant) + + expect(mutation.tenant).toEqual({ + ...input, + id: expect.any(String), + __typename: 'Tenant' + }) + scope.done() + }) + + test('cannot create tenant as non-operator', async (): Promise => { + const input = generateTenantInput() + const tenant = await createTenant(deps) + const apolloClient = createTenantedApolloClient(appContainer, tenant.id) + + try { + await apolloClient + .mutate({ + mutation: gql` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input + } + }) + .then((query): TenantMutationResponse => query.data?.createTenant) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'permission denied', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.Forbidden + }) + }) + ) + } + }) + }) + describe('Update', (): void => { + let tenantedApolloClient: ApolloClient + let tenant: TenantModel + beforeEach(async (): Promise => { + tenant = await createTenant(deps) + tenantedApolloClient = createTenantedApolloClient( + appContainer, + tenant.id + ) + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + + test.each` + isOperator | description + ${true} | ${'operator'} + ${false} | ${'tenant'} + `( + 'can update a tenant as $description', + async ({ isOperator }): Promise => { + const client = isOperator + ? appContainer.apolloClient + : tenantedApolloClient + const updateInput = { + ...generateTenantInput(), + id: tenant.id + } + + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { updateTenant: { id: tenant.id } } }) + const mutation = await client + .mutate({ + mutation: gql` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: updateInput + } + }) + .then((query): TenantMutationResponse => query.data?.updateTenant) + + scope.done() + expect(mutation.tenant).toEqual({ + ...updateInput, + __typename: 'Tenant' + }) + } + ) + test('Cannot update other tenant as non-operator', async (): Promise => { + const firstTenant = await createTenant(deps) + const secondTenant = await createTenant(deps) + + const updateInput = { + ...generateTenantInput(), + id: secondTenant.id + } + const client = createTenantedApolloClient(appContainer, firstTenant.id) + try { + await client + .mutate({ + mutation: gql` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: updateInput + } + }) + .then((query): TenantMutationResponse => query.data?.updateTenant) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'tenant does not exist', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) + }) + + describe('Delete', (): void => { + test.each` + isOperator | description + ${true} | ${'operator'} + ${false} | ${'tenant'} + `( + 'Can delete a tenant as $description', + async ({ isOperator }): Promise => { + const tenant = await createTenant(deps) + + const client = isOperator + ? appContainer.apolloClient + : createTenantedApolloClient(appContainer, tenant.id) + const scope = nock(config.authAdminApiUrl) + .post('') + .reply(200, { data: { deleteTenant: { id: tenant.id } } }) + const mutation = await client + .mutate({ + mutation: gql` + mutation DeleteTenant($id: String!) { + deleteTenant(id: $id) { + success + } + } + `, + variables: { + id: tenant.id + } + }) + .then( + (query): DeleteTenantMutationResponse => query.data?.deleteTenant + ) + + scope.done() + expect(mutation.success).toBe(true) + } + ) + + test('Cannot delete other tenant as non-operator', async (): Promise => { + const firstTenant = await createTenant(deps) + const secondTenant = await createTenant(deps) + + const client = createTenantedApolloClient(appContainer, secondTenant.id) + + try { + await client + .mutate({ + mutation: gql` + mutation DeleteTenant($id: String!) { + deleteTenant(id: $id) { + success + } + } + `, + variables: { + id: firstTenant.id + } + }) + .then( + (query): DeleteTenantMutationResponse => query.data?.deleteTenant + ) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'tenant does not exist', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.NotFound + }) + }) + ) + } + }) + }) + }) +}) diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts new file mode 100644 index 0000000000..6d27940a4e --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -0,0 +1,179 @@ +import { GraphQLError } from 'graphql' +import { TenantedApolloContext } from '../../app' +import { + MutationResolvers, + QueryResolvers, + ResolversTypes, + Tenant as SchemaTenant +} from '../generated/graphql' +import { GraphQLErrorCode } from '../errors' +import { Tenant } from '../../tenants/model' +import { Pagination, SortOrder } from '../../shared/baseModel' +import { getPageInfo } from '../../shared/pagination' + +export const whoami: QueryResolvers['whoami'] = async ( + parent, + args, + ctx +): Promise => { + const { tenant, isOperator } = ctx + + return { + id: tenant.id, + isOperator + } +} + +export const getTenant: QueryResolvers['tenant'] = + async (parent, args, ctx): Promise => { + const { tenant: contextTenant, isOperator } = ctx + + // TODO: make this a util + // If the tenant that was authorized in the request is not the tenant being requested, + // or the requester is not the operator, return not found + if (args.id !== contextTenant.id && !isOperator) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + const tenant = await tenantService.get(args.id) + if (!tenant) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + return tenantToGraphQl(tenant) + } + +export const getTenants: QueryResolvers['tenants'] = + async (parent, args, ctx): Promise => { + const { isOperator } = ctx + if (!isOperator) { + throw new GraphQLError('cannot get tenants page', { + extensions: { + code: GraphQLErrorCode.Forbidden + } + }) + } + + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const tenantService = await ctx.container.use('tenantService') + + const tenants = await tenantService.getPage(pagination, order) + + const pageInfo = await getPageInfo({ + getPage: (pagination: Pagination, sortOrder?: SortOrder) => + tenantService.getPage(pagination, sortOrder), + page: tenants, + sortOrder: order + }) + return { + pageInfo, + edges: tenants.map((tenant: Tenant) => ({ + cursor: tenant.id, + node: tenantToGraphQl(tenant) + })) + } + } + +export const createTenant: MutationResolvers['createTenant'] = + async ( + parent, + args, + ctx + ): Promise => { + // createTenant is an operator-only resolver + const { isOperator } = ctx + if (!isOperator) { + throw new GraphQLError('permission denied', { + extensions: { + code: GraphQLErrorCode.Forbidden + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + const tenant = await tenantService.create(args.input) + + return { tenant: tenantToGraphQl(tenant) } + } + +export const updateTenant: MutationResolvers['updateTenant'] = + async ( + parent, + args, + ctx + ): Promise => { + const { tenant: contextTenant, isOperator } = ctx + // TODO: make this a util + if (args.input.id !== contextTenant.id && !isOperator) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + try { + const updatedTenant = await tenantService.update(args.input) + return { tenant: tenantToGraphQl(updatedTenant) } + } catch (err) { + throw new GraphQLError('failed to update tenant', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + } + +export const deleteTenant: MutationResolvers['deleteTenant'] = + async ( + parent, + args, + ctx + ): Promise => { + const { tenant: contextTenant, isOperator } = ctx + if (args.id !== contextTenant.id && !isOperator) { + throw new GraphQLError('tenant does not exist', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + + const tenantService = await ctx.container.use('tenantService') + try { + await tenantService.delete(args.id) + return { success: true } + } catch (err) { + throw new GraphQLError('failed to delete tenant', { + extensions: { + code: GraphQLErrorCode.NotFound + } + }) + } + } + +export function tenantToGraphQl(tenant: Tenant): SchemaTenant { + return { + id: tenant.id, + email: tenant.email, + apiSecret: tenant.apiSecret, + idpConsentUrl: tenant.idpConsentUrl, + idpSecret: tenant.idpSecret, + publicName: tenant.publicName, + createdAt: new Date(+tenant.createdAt).toISOString(), + deletedAt: tenant.deletedAt + ? new Date(+tenant.deletedAt).toISOString() + : null + } +} diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index f8286e14d4..958c9e9626 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -148,6 +148,26 @@ type Query { "Unique identifier of the receiver (incoming payment URL)." id: String! ): Receiver + + "Retrieve a tenant of the instance." + tenant("Unique identifier of the tenant." id: String!): Tenant + + "Fetch a paginated list of tenants on the instance." + tenants( + "Forward pagination: Cursor (tenant ID) to start retrieving tenants after this point." + after: String + "Backward pagination: Cursor (tenant ID) to start retrieving tenants before this point." + before: String + "Forward pagination: Limit the result to the first **n** tenants after the `after` cursor." + first: Int + "Backward pagination: Limit the result to the last **n** tenants before the `before` cursor." + last: Int + "Specify the sort order of tenants based on their creation date, either ascending or descending." + sortOrder: SortOrder + ): TenantsConnection! + + "Determine if the requester has operator permissions" + whoami: WhoamiResponse! } type Mutation { @@ -306,6 +326,15 @@ type Mutation { cancelIncomingPayment( input: CancelIncomingPaymentInput! ): CancelIncomingPaymentResponse! + + "Create a tenant." + createTenant(input: CreateTenantInput!): TenantMutationResponse! + + "Update a tenant." + updateTenant(input: UpdateTenantInput!): TenantMutationResponse! + + "Delete a tenant." + deleteTenant(id: String!): DeleteTenantMutationResponse! } type PageInfo { @@ -319,6 +348,11 @@ type PageInfo { startCursor: String } +type WhoamiResponse { + id: String! + isOperator: Boolean! +} + type AssetsConnection { "Information to aid in pagination." pageInfo: PageInfo! @@ -1493,6 +1527,75 @@ type CancelIncomingPaymentResponse { payment: IncomingPayment } +type Tenant implements Model { + "Unique identifier of the tenant." + id: ID! + "Contact email of the tenant owner." + email: String! + "Secret used to secure requests made for this tenant." + apiSecret: String! + "URL of the tenant's identity provider's consent screen." + idpConsentUrl: String! + "Secret used to secure requests from the tenant's identity provider." + idpSecret: String! + "Public name for the tenant." + publicName: String + "The date and time that this tenant was created." + createdAt: String! + "The date and time that this tenant was deleted." + deletedAt: String +} + +type TenantsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges representing tenants and cursors for pagination." + edges: [TenantEdge!]! +} + +type TenantEdge { + "A tenant node in the list." + node: Tenant! + "A cursor for paginating through the tenants." + cursor: String! +} + +input CreateTenantInput { + "Contact email of the tenant owner." + email: String! + "Secret used to secure requests made for this tenant." + apiSecret: String! + "URL of the tenant's identity provider's consent screen." + idpConsentUrl: String! + "Secret used to secure requests from the tenant's identity provider." + idpSecret: String! + "Public name for the tenant." + publicName: String +} + +input UpdateTenantInput { + "Unique identifier of the tenant." + id: ID! + "Contact email of the tenant owner." + email: String + "Secret used to secure requests made for this tenant." + apiSecret: String + "URL of the tenant's identity provider's consent screen." + idpConsentUrl: String + "Secret used to secure requests from the tenant's identity provider." + idpSecret: String + "Public name for the tenant." + publicName: String +} + +type TenantMutationResponse { + tenant: Tenant! +} + +type DeleteTenantMutationResponse { + success: Boolean! +} + """ The `UInt8` scalar type represents unsigned 8-bit whole numeric values, ranging from 0 to 255. """ diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 3642de6c1a..4f3d6bab52 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -14,6 +14,7 @@ import { start, gracefulShutdown } from '..' import { onError } from '@apollo/client/link/error' import { App, AppServices } from '../app' +import { Config } from '../config/app' export const testAccessToken = 'test-app-access' diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts index 579735b73d..d8874b4573 100644 --- a/packages/backend/src/tests/tenant.ts +++ b/packages/backend/src/tests/tenant.ts @@ -11,6 +11,16 @@ interface CreateOptions { idpSecret: string } +export function generateTenantInput() { + return { + email: faker.internet.email(), + apiSecret: faker.string.alphanumeric(8), + idpConsentUrl: faker.internet.url(), + idpSecret: faker.string.alphanumeric(8), + publicName: faker.company.name() + } +} + export async function createTenant( deps: IocContract, options?: CreateOptions diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 1116595860..d9a9050ed1 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,6 +2743,7 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 46dad2dedb..0d37f19a34 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -364,6 +364,19 @@ export type CreateReceiverResponse = { receiver?: Maybe; }; +export type CreateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['input']; + /** Contact email of the tenant owner. */ + email: Scalars['String']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['input']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['input']; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type CreateWalletAddressInput = { /** Additional properties associated with the wallet address. */ additionalProperties?: InputMaybe>; @@ -440,6 +453,11 @@ export type DeletePeerMutationResponse = { success: Scalars['Boolean']['output']; }; +export type DeleteTenantMutationResponse = { + __typename?: 'DeleteTenantMutationResponse'; + success: Scalars['Boolean']['output']; +}; + export type DepositAssetLiquidityInput = { /** Amount of liquidity to deposit. */ amount: Scalars['UInt64']['input']; @@ -721,6 +739,8 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; + /** Create a tenant. */ + createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; /** Add a public key to a wallet address that is used to verify Open Payments requests. */ @@ -731,6 +751,8 @@ export type Mutation = { deleteAsset: DeleteAssetMutationResponse; /** Delete a peer. */ deletePeer: DeletePeerMutationResponse; + /** Delete a tenant. */ + deleteTenant: DeleteTenantMutationResponse; /** Deposit asset liquidity. */ depositAssetLiquidity?: Maybe; /** @@ -756,6 +778,8 @@ export type Mutation = { updateIncomingPayment: IncomingPaymentResponse; /** Update an existing peer. */ updatePeer: UpdatePeerMutationResponse; + /** Update a tenant. */ + updateTenant: TenantMutationResponse; /** Update an existing wallet address. */ updateWalletAddress: UpdateWalletAddressMutationResponse; /** Void liquidity withdrawal. Withdrawals are two-phase commits and are rolled back via this mutation. */ @@ -843,6 +867,11 @@ export type MutationCreateReceiverArgs = { }; +export type MutationCreateTenantArgs = { + input: CreateTenantInput; +}; + + export type MutationCreateWalletAddressArgs = { input: CreateWalletAddressInput; }; @@ -868,6 +897,11 @@ export type MutationDeletePeerArgs = { }; +export type MutationDeleteTenantArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDepositAssetLiquidityArgs = { input: DepositAssetLiquidityInput; }; @@ -923,6 +957,11 @@ export type MutationUpdatePeerArgs = { }; +export type MutationUpdateTenantArgs = { + input: UpdateTenantInput; +}; + + export type MutationUpdateWalletAddressArgs = { input: UpdateWalletAddressInput; }; @@ -1150,6 +1189,10 @@ export type Query = { quote?: Maybe; /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; + /** Retrieve a tenant of the instance. */ + tenant?: Maybe; + /** Fetch a paginated list of tenants on the instance. */ + tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; /** Get a wallet address by its url if it exists */ @@ -1158,6 +1201,8 @@ export type Query = { walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ webhookEvents: WebhookEventsConnection; + /** Determine if the requester has operator permissions */ + whoami: WhoamiResponse; }; @@ -1247,6 +1292,20 @@ export type QueryReceiverArgs = { }; +export type QueryTenantArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryTenantsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type QueryWalletAddressArgs = { id: Scalars['String']['input']; }; @@ -1376,6 +1435,47 @@ export enum SortOrder { Desc = 'DESC' } +export type Tenant = Model & { + __typename?: 'Tenant'; + /** Secret used to secure requests made for this tenant. */ + apiSecret: Scalars['String']['output']; + /** The date and time that this tenant was created. */ + createdAt: Scalars['String']['output']; + /** The date and time that this tenant was deleted. */ + deletedAt?: Maybe; + /** Contact email of the tenant owner. */ + email: Scalars['String']['output']; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['output']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl: Scalars['String']['output']; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret: Scalars['String']['output']; + /** Public name for the tenant. */ + publicName?: Maybe; +}; + +export type TenantEdge = { + __typename?: 'TenantEdge'; + /** A cursor for paginating through the tenants. */ + cursor: Scalars['String']['output']; + /** A tenant node in the list. */ + node: Tenant; +}; + +export type TenantMutationResponse = { + __typename?: 'TenantMutationResponse'; + tenant: Tenant; +}; + +export type TenantsConnection = { + __typename?: 'TenantsConnection'; + /** A list of edges representing tenants and cursors for pagination. */ + edges: Array; + /** Information to aid in pagination. */ + pageInfo: PageInfo; +}; + export enum TransferState { /** The accounting transfer is pending */ Pending = 'PENDING', @@ -1448,6 +1548,21 @@ export type UpdatePeerMutationResponse = { peer?: Maybe; }; +export type UpdateTenantInput = { + /** Secret used to secure requests made for this tenant. */ + apiSecret?: InputMaybe; + /** Contact email of the tenant owner. */ + email?: InputMaybe; + /** Unique identifier of the tenant. */ + id: Scalars['ID']['input']; + /** URL of the tenant's identity provider's consent screen. */ + idpConsentUrl?: InputMaybe; + /** Secret used to secure requests from the tenant's identity provider. */ + idpSecret?: InputMaybe; + /** Public name for the tenant. */ + publicName?: InputMaybe; +}; + export type UpdateWalletAddressInput = { /** Additional properties associated with this wallet address. */ additionalProperties?: InputMaybe>; @@ -1640,6 +1755,12 @@ export type WebhookEventsEdge = { node: WebhookEvent; }; +export type WhoamiResponse = { + __typename?: 'WhoamiResponse'; + id: Scalars['String']['output']; + isOperator: Scalars['Boolean']['output']; +}; + export type WithdrawEventLiquidityInput = { /** Unique identifier of the event to withdraw liquidity from. */ eventId: Scalars['String']['input']; @@ -1718,7 +1839,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1756,6 +1877,7 @@ export type ResolversTypes = { CreateQuoteInput: ResolverTypeWrapper>; CreateReceiverInput: ResolverTypeWrapper>; CreateReceiverResponse: ResolverTypeWrapper>; + CreateTenantInput: ResolverTypeWrapper>; CreateWalletAddressInput: ResolverTypeWrapper>; CreateWalletAddressKeyInput: ResolverTypeWrapper>; CreateWalletAddressKeyMutationResponse: ResolverTypeWrapper>; @@ -1766,6 +1888,7 @@ export type ResolversTypes = { DeleteAssetMutationResponse: ResolverTypeWrapper>; DeletePeerInput: ResolverTypeWrapper>; DeletePeerMutationResponse: ResolverTypeWrapper>; + DeleteTenantMutationResponse: ResolverTypeWrapper>; DepositAssetLiquidityInput: ResolverTypeWrapper>; DepositEventLiquidityInput: ResolverTypeWrapper>; DepositOutgoingPaymentLiquidityInput: ResolverTypeWrapper>; @@ -1825,6 +1948,10 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + Tenant: ResolverTypeWrapper>; + TenantEdge: ResolverTypeWrapper>; + TenantMutationResponse: ResolverTypeWrapper>; + TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; @@ -1835,6 +1962,7 @@ export type ResolversTypes = { UpdateIncomingPaymentInput: ResolverTypeWrapper>; UpdatePeerInput: ResolverTypeWrapper>; UpdatePeerMutationResponse: ResolverTypeWrapper>; + UpdateTenantInput: ResolverTypeWrapper>; UpdateWalletAddressInput: ResolverTypeWrapper>; UpdateWalletAddressMutationResponse: ResolverTypeWrapper>; VoidLiquidityWithdrawalInput: ResolverTypeWrapper>; @@ -1851,6 +1979,7 @@ export type ResolversTypes = { WebhookEventFilter: ResolverTypeWrapper>; WebhookEventsConnection: ResolverTypeWrapper>; WebhookEventsEdge: ResolverTypeWrapper>; + WhoamiResponse: ResolverTypeWrapper>; WithdrawEventLiquidityInput: ResolverTypeWrapper>; }; @@ -1888,6 +2017,7 @@ export type ResolversParentTypes = { CreateQuoteInput: Partial; CreateReceiverInput: Partial; CreateReceiverResponse: Partial; + CreateTenantInput: Partial; CreateWalletAddressInput: Partial; CreateWalletAddressKeyInput: Partial; CreateWalletAddressKeyMutationResponse: Partial; @@ -1897,6 +2027,7 @@ export type ResolversParentTypes = { DeleteAssetMutationResponse: Partial; DeletePeerInput: Partial; DeletePeerMutationResponse: Partial; + DeleteTenantMutationResponse: Partial; DepositAssetLiquidityInput: Partial; DepositEventLiquidityInput: Partial; DepositOutgoingPaymentLiquidityInput: Partial; @@ -1949,6 +2080,10 @@ export type ResolversParentTypes = { SetFeeInput: Partial; SetFeeResponse: Partial; String: Partial; + Tenant: Partial; + TenantEdge: Partial; + TenantMutationResponse: Partial; + TenantsConnection: Partial; TriggerWalletAddressEventsInput: Partial; TriggerWalletAddressEventsMutationResponse: Partial; UInt8: Partial; @@ -1957,6 +2092,7 @@ export type ResolversParentTypes = { UpdateIncomingPaymentInput: Partial; UpdatePeerInput: Partial; UpdatePeerMutationResponse: Partial; + UpdateTenantInput: Partial; UpdateWalletAddressInput: Partial; UpdateWalletAddressMutationResponse: Partial; VoidLiquidityWithdrawalInput: Partial; @@ -1972,6 +2108,7 @@ export type ResolversParentTypes = { WebhookEventFilter: Partial; WebhookEventsConnection: Partial; WebhookEventsEdge: Partial; + WhoamiResponse: Partial; WithdrawEventLiquidityInput: Partial; }; @@ -2093,6 +2230,11 @@ export type DeletePeerMutationResponseResolvers; }; +export type DeleteTenantMutationResponseResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeeResolvers = { assetId?: Resolver; basisPoints?: Resolver; @@ -2176,7 +2318,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2197,11 +2339,13 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; createQuote?: Resolver>; createReceiver?: Resolver>; + createTenant?: Resolver>; createWalletAddress?: Resolver>; createWalletAddressKey?: Resolver, ParentType, ContextType, RequireFields>; createWalletAddressWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; deleteAsset?: Resolver>; deletePeer?: Resolver>; + deleteTenant?: Resolver>; depositAssetLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; depositOutgoingPaymentLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2213,6 +2357,7 @@ export type MutationResolvers>; updateIncomingPayment?: Resolver>; updatePeer?: Resolver>; + updateTenant?: Resolver>; updateWalletAddress?: Resolver>; voidLiquidityWithdrawal?: Resolver, ParentType, ContextType, RequireFields>; withdrawEventLiquidity?: Resolver, ParentType, ContextType, RequireFields>; @@ -2325,10 +2470,13 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; + whoami?: Resolver; }; export type QuoteResolvers = { @@ -2383,6 +2531,35 @@ export type SetFeeResponseResolvers; }; +export type TenantResolvers = { + apiSecret?: Resolver; + createdAt?: Resolver; + deletedAt?: Resolver, ParentType, ContextType>; + email?: Resolver; + id?: Resolver; + idpConsentUrl?: Resolver; + idpSecret?: Resolver; + publicName?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantMutationResponseResolvers = { + tenant?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type TenantsConnectionResolvers = { + edges?: Resolver, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TriggerWalletAddressEventsMutationResponseResolvers = { count?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2487,6 +2664,12 @@ export type WebhookEventsEdgeResolvers; }; +export type WhoamiResponseResolvers = { + id?: Resolver; + isOperator?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { AccountingTransfer?: AccountingTransferResolvers; AccountingTransferConnection?: AccountingTransferConnectionResolvers; @@ -2506,6 +2689,7 @@ export type Resolvers = { CreateWalletAddressMutationResponse?: CreateWalletAddressMutationResponseResolvers; DeleteAssetMutationResponse?: DeleteAssetMutationResponseResolvers; DeletePeerMutationResponse?: DeletePeerMutationResponseResolvers; + DeleteTenantMutationResponse?: DeleteTenantMutationResponseResolvers; Fee?: FeeResolvers; FeeEdge?: FeeEdgeResolvers; FeesConnection?: FeesConnectionResolvers; @@ -2539,6 +2723,10 @@ export type Resolvers = { Receiver?: ReceiverResolvers; RevokeWalletAddressKeyMutationResponse?: RevokeWalletAddressKeyMutationResponseResolvers; SetFeeResponse?: SetFeeResponseResolvers; + Tenant?: TenantResolvers; + TenantEdge?: TenantEdgeResolvers; + TenantMutationResponse?: TenantMutationResponseResolvers; + TenantsConnection?: TenantsConnectionResolvers; TriggerWalletAddressEventsMutationResponse?: TriggerWalletAddressEventsMutationResponseResolvers; UInt8?: GraphQLScalarType; UInt64?: GraphQLScalarType; @@ -2555,5 +2743,6 @@ export type Resolvers = { WebhookEvent?: WebhookEventResolvers; WebhookEventsConnection?: WebhookEventsConnectionResolvers; WebhookEventsEdge?: WebhookEventsEdgeResolvers; + WhoamiResponse?: WhoamiResponseResolvers; }; From 344e21af432e26ce3db3fe83f8a687e105d14dab Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 21 Jan 2025 14:20:24 -0800 Subject: [PATCH 02/10] chore: formatting --- packages/backend/src/tests/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 4f3d6bab52..3642de6c1a 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -14,7 +14,6 @@ import { start, gracefulShutdown } from '..' import { onError } from '@apollo/client/link/error' import { App, AppServices } from '../app' -import { Config } from '../config/app' export const testAccessToken = 'test-app-access' From 657780fa5c5d5600e4d9d4eba98e6bba52641197 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 22 Jan 2025 14:12:55 -0800 Subject: [PATCH 03/10] fix: extra testing db for tenants --- packages/backend/jest.setup.ts | 31 +++++++++++++++++++ packages/backend/jest.teardown.js | 5 +++ packages/backend/knexfile.js | 16 ++++++++++ packages/backend/scripts/init.sh | 2 ++ .../src/graphql/resolvers/tenant.test.ts | 30 +++++++++--------- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/packages/backend/jest.setup.ts b/packages/backend/jest.setup.ts index ef4340581d..a12bfa1585 100644 --- a/packages/backend/jest.setup.ts +++ b/packages/backend/jest.setup.ts @@ -34,6 +34,10 @@ const setup = async (globalConfig): Promise => { POSTGRES_PORT )}/testing` + process.env.TENANT_TEST_DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort( + POSTGRES_PORT + )}/tenant_testing` + global.__BACKEND_POSTGRES__ = postgresContainer } @@ -49,6 +53,18 @@ const setup = async (globalConfig): Promise => { } }) + const tenantDb = knex({ + client: 'postgresql', + connection: process.env.TENANT_TEST_DATABASE_URL, + pool: { + min: 2, + max: 10 + }, + migrations: { + tableName: 'knex_migrations' + } + }) + // node pg defaults to returning bigint as string. This ensures it parses to bigint db.client.driver.types.setTypeParser( db.client.driver.types.builtins.INT8, @@ -59,14 +75,29 @@ const setup = async (globalConfig): Promise => { directory: __dirname + '/migrations' }) + tenantDb.client.driver.types.setTypeParser( + tenantDb.client.driver.types.builtins.INT8, + 'text', + BigInt + ) + await tenantDb.migrate.latest({ + directory: __dirname + '/migrations' + }) + for (let i = 1; i <= workers; i++) { const workerDatabaseName = `testing_${i}` + const tenantWorkerDatabaseName = `tenant_testing_${i}` await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`) + await tenantDb.raw(`DROP DATABASE IF EXISTS ${tenantWorkerDatabaseName}`) await db.raw(`CREATE DATABASE ${workerDatabaseName} TEMPLATE testing`) + await tenantDb.raw( + `CREATE DATABASE ${tenantWorkerDatabaseName} TEMPLATE tenant_testing` + ) } global.__BACKEND_KNEX__ = db + global.__BACKEND_TENANT_KNEX__ = tenantDb } const setupRedis = async () => { diff --git a/packages/backend/jest.teardown.js b/packages/backend/jest.teardown.js index 00020f301f..adc1aab423 100644 --- a/packages/backend/jest.teardown.js +++ b/packages/backend/jest.teardown.js @@ -3,7 +3,12 @@ module.exports = async () => { { directory: __dirname + '/migrations' }, true ) + await global.__BACKEND_TENANT_KNEX__.migrate.rollback( + { directory: __dirname + '/migrations' }, + true + ) await global.__BACKEND_KNEX__.destroy() + await global.__BACKEND_TENANT_KNEX__.destroy() if (global.__BACKEND_POSTGRES__) { await global.__BACKEND_POSTGRES__.stop() } diff --git a/packages/backend/knexfile.js b/packages/backend/knexfile.js index 6f8e2606ed..e036ca74c1 100644 --- a/packages/backend/knexfile.js +++ b/packages/backend/knexfile.js @@ -33,6 +33,22 @@ module.exports = { } }, + tenant_testing: { + client: 'postgresql', + connection: { + database: 'tenant_testing', + user: 'postgres', + password: 'password' + }, + pool: { + min: 2, + max: 10 + }, + migrations: { + tableName: 'knex_migrations' + } + }, + production: { client: 'postgresql', connection: process.env.DATABASE_URL, diff --git a/packages/backend/scripts/init.sh b/packages/backend/scripts/init.sh index a1fa255c7d..6f3b607aa9 100755 --- a/packages/backend/scripts/init.sh +++ b/packages/backend/scripts/init.sh @@ -3,6 +3,8 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL DROP DATABASE IF EXISTS TESTING; + DROP DATABASE IF EXISTS TENANT_TESTING; CREATE DATABASE testing; + CREATE DATABASE tenant_testing; CREATE DATABASE development; EOSQL diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 4882d8b7bd..50c54eaf89 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -14,7 +14,6 @@ import { createTenant, generateTenantInput } from '../../tests/tenant' import { ApolloError, gql, NormalizedCacheObject } from '@apollo/client' import { getPageTests } from './page.test' import { truncateTables } from '../../tests/tableManager' -import nock from 'nock' import { createHttpLink, ApolloLink, @@ -67,13 +66,26 @@ describe('Tenant Resolvers', (): void => { let config: IAppConfig beforeAll(async (): Promise => { - deps = await initIocContainer(Config) + deps = await initIocContainer({ + ...Config, + databaseUrl: process.env.TENANT_TEST_DATABASE_URL as string + }) appContainer = await createTestApp(deps) config = await deps.use('config') + const authServiceClient = await deps.use('authServiceClient') + jest + .spyOn(authServiceClient.tenant, 'create') + .mockImplementation(async () => undefined) + jest + .spyOn(authServiceClient.tenant, 'update') + .mockImplementation(async () => undefined) + jest + .spyOn(authServiceClient.tenant, 'delete') + .mockImplementation(async () => undefined) }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(appContainer.knex, true) }) afterAll(async (): Promise => { await appContainer.apolloClient.stop() @@ -249,9 +261,6 @@ describe('Tenant Resolvers', (): void => { describe('Create', (): void => { test('can create a tenant', async (): Promise => { const input = generateTenantInput() - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { createTenant: { id: 1234 } } }) const mutation = await appContainer.apolloClient .mutate({ @@ -280,7 +289,6 @@ describe('Tenant Resolvers', (): void => { id: expect.any(String), __typename: 'Tenant' }) - scope.done() }) test('cannot create tenant as non-operator', async (): Promise => { @@ -353,9 +361,6 @@ describe('Tenant Resolvers', (): void => { id: tenant.id } - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { updateTenant: { id: tenant.id } } }) const mutation = await client .mutate({ mutation: gql` @@ -378,7 +383,6 @@ describe('Tenant Resolvers', (): void => { }) .then((query): TenantMutationResponse => query.data?.updateTenant) - scope.done() expect(mutation.tenant).toEqual({ ...updateInput, __typename: 'Tenant' @@ -443,9 +447,6 @@ describe('Tenant Resolvers', (): void => { const client = isOperator ? appContainer.apolloClient : createTenantedApolloClient(appContainer, tenant.id) - const scope = nock(config.authAdminApiUrl) - .post('') - .reply(200, { data: { deleteTenant: { id: tenant.id } } }) const mutation = await client .mutate({ mutation: gql` @@ -463,7 +464,6 @@ describe('Tenant Resolvers', (): void => { (query): DeleteTenantMutationResponse => query.data?.deleteTenant ) - scope.done() expect(mutation.success).toBe(true) } ) From 3ce91282c3027747478c5183bbdf05f1ac7c635b Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Thu, 23 Jan 2025 14:21:55 -0800 Subject: [PATCH 04/10] feat: bruno collection --- .../Rafiki Admin APIs/Create Tenant.bru | 50 +++++++++++++++++++ .../Rafiki Admin APIs/Delete Tenant.bru | 31 ++++++++++++ .../Rafiki Admin APIs/Update Tenant.bru | 43 ++++++++++++++++ .../Rafiki/environments/Local Playground.bru | 1 + bruno/collections/Rafiki/scripts.js | 1 + 5 files changed, 126 insertions(+) create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru create mode 100644 bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru new file mode 100644 index 0000000000..c078d15371 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Create Tenant.bru @@ -0,0 +1,50 @@ +meta { + name: Create Tenant + type: graphql + seq: 54 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input:$input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + } + } + } +} + +body:graphql:vars { + { + "input": { + "email": "example@example.com", + "apiSecret": "test-secret", + "idpConsentUrl": "https://example.com/consent", + "idpSecret": "test-idp-secret" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("tenantId", body.data.createTenant.tenant?.id); + } +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru new file mode 100644 index 0000000000..6c664049f7 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Delete Tenant.bru @@ -0,0 +1,31 @@ +meta { + name: Delete Tenant + type: graphql + seq: 56 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation DeleteTenant($id: String!) { + deleteTenant(id:$id) { + success + } + } +} + +body:graphql:vars { + { + "id": "{{tenantId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru new file mode 100644 index 0000000000..b3f71689d8 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Update Tenant.bru @@ -0,0 +1,43 @@ +meta { + name: Update Tenant + type: graphql + seq: 55 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input:$input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + } + } + } +} + +body:graphql:vars { + { + "input": { + "id": "{{tenantId}}", + "email": "updated@example.com", + "apiSecret": "updated-test-secret", + "idpConsentUrl": "https://example.com/consent-updated", + "idpSecret": "updated-test-idp-secret" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/environments/Local Playground.bru b/bruno/collections/Rafiki/environments/Local Playground.bru index 6cb1033ff6..24924232e4 100644 --- a/bruno/collections/Rafiki/environments/Local Playground.bru +++ b/bruno/collections/Rafiki/environments/Local Playground.bru @@ -30,4 +30,5 @@ vars { assetIdTigerBeetle: 1 assetCode: USD assetScale: 2 + senderTenantId: 438fa74a-fa7d-4317-9ced-dde32ece1787 } diff --git a/bruno/collections/Rafiki/scripts.js b/bruno/collections/Rafiki/scripts.js index 8ea6b7a614..ef03f9ab1d 100644 --- a/bruno/collections/Rafiki/scripts.js +++ b/bruno/collections/Rafiki/scripts.js @@ -127,6 +127,7 @@ const scripts = { signature = this.generateBackendApiSignature(formattedBody) } req.setHeader('signature', signature) + req.setHeader('tenant-id', bru.getEnvVar('senderTenantId')) }, addHostHeader: function (hostVarName) { From 15cb3876ffa49dbf7601d88e5d3c002c76652ea9 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Fri, 24 Jan 2025 13:26:49 -0800 Subject: [PATCH 05/10] feat: update graphql schema comments --- localenv/mock-account-servicing-entity/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/generated/graphql.schema.json | 4 ++-- packages/backend/src/graphql/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/schema.graphql | 4 ++-- packages/frontend/app/generated/graphql.ts | 4 ++-- packages/mock-account-service-lib/src/generated/graphql.ts | 4 ++-- test/integration/lib/generated/graphql.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -739,7 +739,7 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; - /** Create a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index c745f24e15..da5a837a0e 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -4610,7 +4610,7 @@ }, { "name": "createTenant", - "description": "Create a tenant.", + "description": "As an operator, create a tenant.", "args": [ { "name": "input", @@ -7059,7 +7059,7 @@ }, { "name": "tenants", - "description": "Fetch a paginated list of tenants on the instance.", + "description": "As an operator, fetch a paginated list of tenants on the instance.", "args": [ { "name": "after", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -739,7 +739,7 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; - /** Create a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 958c9e9626..2e55d937c5 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -152,7 +152,7 @@ type Query { "Retrieve a tenant of the instance." tenant("Unique identifier of the tenant." id: String!): Tenant - "Fetch a paginated list of tenants on the instance." + "As an operator, fetch a paginated list of tenants on the instance." tenants( "Forward pagination: Cursor (tenant ID) to start retrieving tenants after this point." after: String @@ -327,7 +327,7 @@ type Mutation { input: CancelIncomingPaymentInput! ): CancelIncomingPaymentResponse! - "Create a tenant." + "As an operator, create a tenant." createTenant(input: CreateTenantInput!): TenantMutationResponse! "Update a tenant." diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index d9a9050ed1..4475460428 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -739,7 +739,7 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; - /** Create a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -739,7 +739,7 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; - /** Create a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 0d37f19a34..8d6aac067a 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -739,7 +739,7 @@ export type Mutation = { createQuote: QuoteResponse; /** Create an internal or external Open Payments incoming payment. The receiver has a wallet address on either this or another Open Payments resource server. */ createReceiver: CreateReceiverResponse; - /** Create a tenant. */ + /** As an operator, create a tenant. */ createTenant: TenantMutationResponse; /** Create a new wallet address. */ createWalletAddress: CreateWalletAddressMutationResponse; @@ -1191,7 +1191,7 @@ export type Query = { receiver?: Maybe; /** Retrieve a tenant of the instance. */ tenant?: Maybe; - /** Fetch a paginated list of tenants on the instance. */ + /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; From 4aafebb9e3cc00a402416e94b1c43b518ba23ec0 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 27 Jan 2025 11:25:04 -0800 Subject: [PATCH 06/10] fix: review comments --- .../generated/graphql.ts | 18 +++--- packages/backend/jest.setup.ts | 31 ---------- packages/backend/jest.teardown.js | 5 -- packages/backend/knexfile.js | 16 ----- .../src/graphql/generated/graphql.schema.json | 60 ++++++------------- .../backend/src/graphql/generated/graphql.ts | 18 +++--- .../src/graphql/resolvers/tenant.test.ts | 7 ++- packages/backend/src/graphql/schema.graphql | 12 ++-- packages/frontend/app/generated/graphql.ts | 18 +++--- .../src/generated/graphql.ts | 18 +++--- test/integration/lib/generated/graphql.ts | 18 +++--- 11 files changed, 75 insertions(+), 146 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/jest.setup.ts b/packages/backend/jest.setup.ts index a12bfa1585..ef4340581d 100644 --- a/packages/backend/jest.setup.ts +++ b/packages/backend/jest.setup.ts @@ -34,10 +34,6 @@ const setup = async (globalConfig): Promise => { POSTGRES_PORT )}/testing` - process.env.TENANT_TEST_DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort( - POSTGRES_PORT - )}/tenant_testing` - global.__BACKEND_POSTGRES__ = postgresContainer } @@ -53,18 +49,6 @@ const setup = async (globalConfig): Promise => { } }) - const tenantDb = knex({ - client: 'postgresql', - connection: process.env.TENANT_TEST_DATABASE_URL, - pool: { - min: 2, - max: 10 - }, - migrations: { - tableName: 'knex_migrations' - } - }) - // node pg defaults to returning bigint as string. This ensures it parses to bigint db.client.driver.types.setTypeParser( db.client.driver.types.builtins.INT8, @@ -75,29 +59,14 @@ const setup = async (globalConfig): Promise => { directory: __dirname + '/migrations' }) - tenantDb.client.driver.types.setTypeParser( - tenantDb.client.driver.types.builtins.INT8, - 'text', - BigInt - ) - await tenantDb.migrate.latest({ - directory: __dirname + '/migrations' - }) - for (let i = 1; i <= workers; i++) { const workerDatabaseName = `testing_${i}` - const tenantWorkerDatabaseName = `tenant_testing_${i}` await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`) - await tenantDb.raw(`DROP DATABASE IF EXISTS ${tenantWorkerDatabaseName}`) await db.raw(`CREATE DATABASE ${workerDatabaseName} TEMPLATE testing`) - await tenantDb.raw( - `CREATE DATABASE ${tenantWorkerDatabaseName} TEMPLATE tenant_testing` - ) } global.__BACKEND_KNEX__ = db - global.__BACKEND_TENANT_KNEX__ = tenantDb } const setupRedis = async () => { diff --git a/packages/backend/jest.teardown.js b/packages/backend/jest.teardown.js index adc1aab423..00020f301f 100644 --- a/packages/backend/jest.teardown.js +++ b/packages/backend/jest.teardown.js @@ -3,12 +3,7 @@ module.exports = async () => { { directory: __dirname + '/migrations' }, true ) - await global.__BACKEND_TENANT_KNEX__.migrate.rollback( - { directory: __dirname + '/migrations' }, - true - ) await global.__BACKEND_KNEX__.destroy() - await global.__BACKEND_TENANT_KNEX__.destroy() if (global.__BACKEND_POSTGRES__) { await global.__BACKEND_POSTGRES__.stop() } diff --git a/packages/backend/knexfile.js b/packages/backend/knexfile.js index e036ca74c1..6f8e2606ed 100644 --- a/packages/backend/knexfile.js +++ b/packages/backend/knexfile.js @@ -33,22 +33,6 @@ module.exports = { } }, - tenant_testing: { - client: 'postgresql', - connection: { - database: 'tenant_testing', - user: 'postgres', - password: 'password' - }, - pool: { - min: 2, - max: 10 - }, - migrations: { - tableName: 'knex_migrations' - } - }, - production: { client: 'postgresql', connection: process.env.DATABASE_URL, diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index da5a837a0e..a843adf06c 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2131,13 +2131,9 @@ "name": "email", "description": "Contact email of the tenant owner.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -2147,13 +2143,9 @@ "name": "idpConsentUrl", "description": "URL of the tenant's identity provider's consent screen.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -2163,13 +2155,9 @@ "name": "idpSecret", "description": "Secret used to secure requests from the tenant's identity provider.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -8018,13 +8006,9 @@ "description": "Contact email of the tenant owner.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -8050,13 +8034,9 @@ "description": "URL of the tenant's identity provider's consent screen.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -8066,13 +8046,9 @@ "description": "Secret used to secure requests from the tenant's identity provider.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 50c54eaf89..bb706dc650 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -68,7 +68,7 @@ describe('Tenant Resolvers', (): void => { beforeAll(async (): Promise => { deps = await initIocContainer({ ...Config, - databaseUrl: process.env.TENANT_TEST_DATABASE_URL as string + dbSchema: 'tenant_service_test_schema' }) appContainer = await createTestApp(deps) config = await deps.use('config') @@ -136,6 +136,7 @@ describe('Tenant Resolvers', (): void => { const tenant = await createTenant(deps) const apolloClient = createTenantedApolloClient(appContainer, tenant.id) try { + expect.assertions(2) await apolloClient .query({ query: gql` @@ -228,6 +229,7 @@ describe('Tenant Resolvers', (): void => { ) try { + expect.assertions(2) await apolloClient .query({ query: gql` @@ -297,6 +299,7 @@ describe('Tenant Resolvers', (): void => { const apolloClient = createTenantedApolloClient(appContainer, tenant.id) try { + expect.assertions(2) await apolloClient .mutate({ mutation: gql` @@ -399,6 +402,7 @@ describe('Tenant Resolvers', (): void => { } const client = createTenantedApolloClient(appContainer, firstTenant.id) try { + expect.assertions(2) await client .mutate({ mutation: gql` @@ -475,6 +479,7 @@ describe('Tenant Resolvers', (): void => { const client = createTenantedApolloClient(appContainer, secondTenant.id) try { + expect.assertions(2) await client .mutate({ mutation: gql` diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 2e55d937c5..a80b8906a2 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1531,13 +1531,13 @@ type Tenant implements Model { "Unique identifier of the tenant." id: ID! "Contact email of the tenant owner." - email: String! + email: String "Secret used to secure requests made for this tenant." apiSecret: String! "URL of the tenant's identity provider's consent screen." - idpConsentUrl: String! + idpConsentUrl: String "Secret used to secure requests from the tenant's identity provider." - idpSecret: String! + idpSecret: String "Public name for the tenant." publicName: String "The date and time that this tenant was created." @@ -1562,13 +1562,13 @@ type TenantEdge { input CreateTenantInput { "Contact email of the tenant owner." - email: String! + email: String "Secret used to secure requests made for this tenant." apiSecret: String! "URL of the tenant's identity provider's consent screen." - idpConsentUrl: String! + idpConsentUrl: String "Secret used to secure requests from the tenant's identity provider." - idpSecret: String! + idpSecret: String "Public name for the tenant." publicName: String } diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 4475460428..d7019b0127 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 8d6aac067a..2fe726420e 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -368,11 +368,11 @@ export type CreateTenantInput = { /** Secret used to secure requests made for this tenant. */ apiSecret: Scalars['String']['input']; /** Contact email of the tenant owner. */ - email: Scalars['String']['input']; + email?: InputMaybe; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['input']; + idpConsentUrl?: InputMaybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['input']; + idpSecret?: InputMaybe; /** Public name for the tenant. */ publicName?: InputMaybe; }; @@ -1444,13 +1444,13 @@ export type Tenant = Model & { /** The date and time that this tenant was deleted. */ deletedAt?: Maybe; /** Contact email of the tenant owner. */ - email: Scalars['String']['output']; + email?: Maybe; /** Unique identifier of the tenant. */ id: Scalars['ID']['output']; /** URL of the tenant's identity provider's consent screen. */ - idpConsentUrl: Scalars['String']['output']; + idpConsentUrl?: Maybe; /** Secret used to secure requests from the tenant's identity provider. */ - idpSecret: Scalars['String']['output']; + idpSecret?: Maybe; /** Public name for the tenant. */ publicName?: Maybe; }; @@ -2535,10 +2535,10 @@ export type TenantResolvers; createdAt?: Resolver; deletedAt?: Resolver, ParentType, ContextType>; - email?: Resolver; + email?: Resolver, ParentType, ContextType>; id?: Resolver; - idpConsentUrl?: Resolver; - idpSecret?: Resolver; + idpConsentUrl?: Resolver, ParentType, ContextType>; + idpSecret?: Resolver, ParentType, ContextType>; publicName?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; From 9072e020ad3459aef7dc4c42db0d03816a03357a Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 28 Jan 2025 10:29:56 -0800 Subject: [PATCH 07/10] feat: optional idp secret & consent url --- .../migrations/20241125233415_create_tenants_table.js | 4 ++-- packages/auth/src/tenant/model.ts | 4 ++-- packages/auth/src/tenant/routes.ts | 8 ++++---- packages/auth/src/tenant/service.ts | 4 ++-- packages/backend/jest.env.js | 2 +- packages/backend/src/auth-service-client/client.ts | 4 ++-- packages/backend/src/graphql/resolvers/tenant.test.ts | 1 + packages/backend/src/tenants/service.ts | 6 +++--- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/auth/migrations/20241125233415_create_tenants_table.js b/packages/auth/migrations/20241125233415_create_tenants_table.js index 9112108977..4846a07ce9 100644 --- a/packages/auth/migrations/20241125233415_create_tenants_table.js +++ b/packages/auth/migrations/20241125233415_create_tenants_table.js @@ -5,8 +5,8 @@ exports.up = function (knex) { return knex.schema.createTable('tenants', function (table) { table.uuid('id').notNullable().primary() - table.string('idpConsentUrl').notNullable() - table.string('idpSecret').notNullable() + table.string('idpConsentUrl') + table.string('idpSecret') table.timestamp('createdAt').defaultTo(knex.fn.now()) table.timestamp('updatedAt').defaultTo(knex.fn.now()) diff --git a/packages/auth/src/tenant/model.ts b/packages/auth/src/tenant/model.ts index b135541c34..412422e217 100644 --- a/packages/auth/src/tenant/model.ts +++ b/packages/auth/src/tenant/model.ts @@ -5,8 +5,8 @@ export class Tenant extends BaseModel { return 'tenants' } - public idpConsentUrl!: string - public idpSecret!: string + public idpConsentUrl?: string + public idpSecret?: string public deletedAt?: Date } diff --git a/packages/auth/src/tenant/routes.ts b/packages/auth/src/tenant/routes.ts index 0ffe7c5940..895e07cf2c 100644 --- a/packages/auth/src/tenant/routes.ts +++ b/packages/auth/src/tenant/routes.ts @@ -22,8 +22,8 @@ type TenantContext = Exclude< interface CreateTenantBody { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } type UpdateTenantBody = Partial> @@ -34,8 +34,8 @@ interface TenantParams { interface TenantResponse { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } export type GetContext = TenantContext diff --git a/packages/auth/src/tenant/service.ts b/packages/auth/src/tenant/service.ts index d4f3cc336a..27e562e397 100644 --- a/packages/auth/src/tenant/service.ts +++ b/packages/auth/src/tenant/service.ts @@ -4,8 +4,8 @@ import { Tenant } from './model' export interface CreateOptions { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } export interface TenantService { diff --git a/packages/backend/jest.env.js b/packages/backend/jest.env.js index 4a8435dd72..8b804444ca 100644 --- a/packages/backend/jest.env.js +++ b/packages/backend/jest.env.js @@ -1,4 +1,4 @@ -process.env.LOG_LEVEL = 'silent' +process.env.LOG_LEVEL = 'info' process.env.INSTANCE_NAME = 'Rafiki' process.env.KEY_ID = 'myKey' process.env.OPEN_PAYMENTS_URL = 'http://127.0.0.1:3000' diff --git a/packages/backend/src/auth-service-client/client.ts b/packages/backend/src/auth-service-client/client.ts index da2e0a9a72..402435446f 100644 --- a/packages/backend/src/auth-service-client/client.ts +++ b/packages/backend/src/auth-service-client/client.ts @@ -1,7 +1,7 @@ interface Tenant { id: string - idpConsentUrl: string - idpSecret: string + idpConsentUrl?: string + idpSecret?: string } export class AuthServiceClientError extends Error { diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index bb706dc650..2e1f9ad889 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -99,6 +99,7 @@ describe('Tenant Resolvers', (): void => { ${false} | ${'tenant'} `('whoami query as $description', async ({ isOperator }): Promise => { const tenant = await createTenant(deps) + console.log('created tenant') const client = isOperator ? appContainer.apolloClient : createTenantedApolloClient(appContainer, tenant.id) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 947881619f..11eedc73ca 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -60,10 +60,10 @@ async function getTenantPage( } interface CreateTenantOptions { - email: string + email?: string apiSecret: string - idpSecret: string - idpConsentUrl: string + idpSecret?: string + idpConsentUrl?: string publicName?: string } From e4a6e6ca5d47b949dd2c5e30faf50671d4b44bcc Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 28 Jan 2025 11:09:59 -0800 Subject: [PATCH 08/10] feat: tenant response requirement --- .../mock-account-servicing-entity/generated/graphql.ts | 4 ++-- .../backend/src/graphql/generated/graphql.schema.json | 10 +++++++--- packages/backend/src/graphql/generated/graphql.ts | 4 ++-- packages/backend/src/graphql/schema.graphql | 2 +- packages/frontend/app/generated/graphql.ts | 4 ++-- .../mock-account-service-lib/src/generated/graphql.ts | 4 ++-- test/integration/lib/generated/graphql.ts | 4 ++-- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index a843adf06c..ed648e8445 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -7038,9 +7038,13 @@ } ], "type": { - "kind": "OBJECT", - "name": "Tenant", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tenant", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index a80b8906a2..fe1e4354c5 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -150,7 +150,7 @@ type Query { ): Receiver "Retrieve a tenant of the instance." - tenant("Unique identifier of the tenant." id: String!): Tenant + tenant("Unique identifier of the tenant." id: String!): Tenant! "As an operator, fetch a paginated list of tenants on the instance." tenants( diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index d7019b0127..b8fd92871d 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 2fe726420e..a9a4b15bc0 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1190,7 +1190,7 @@ export type Query = { /** Retrieve an Open Payments incoming payment by receiver ID. The receiver's wallet address can be hosted on this server or a remote Open Payments resource server. */ receiver?: Maybe; /** Retrieve a tenant of the instance. */ - tenant?: Maybe; + tenant: Tenant; /** As an operator, fetch a paginated list of tenants on the instance. */ tenants: TenantsConnection; /** Fetch a wallet address by its ID. */ @@ -2470,7 +2470,7 @@ export type QueryResolvers>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; - tenant?: Resolver, ParentType, ContextType, RequireFields>; + tenant?: Resolver>; tenants?: Resolver>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; From 45dcb5b4ea8c573236d635c70389f083cf26745c Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Tue, 28 Jan 2025 11:39:38 -0800 Subject: [PATCH 09/10] feat: make delete operator-only --- .../src/graphql/resolvers/tenant.test.ts | 52 ++++++++----------- .../backend/src/graphql/resolvers/tenant.ts | 8 +-- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 2e1f9ad889..628be01be6 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -440,40 +440,30 @@ describe('Tenant Resolvers', (): void => { }) describe('Delete', (): void => { - test.each` - isOperator | description - ${true} | ${'operator'} - ${false} | ${'tenant'} - `( - 'Can delete a tenant as $description', - async ({ isOperator }): Promise => { - const tenant = await createTenant(deps) + test('Can delete a tenant as operator', async (): Promise => { + const tenant = await createTenant(deps) - const client = isOperator - ? appContainer.apolloClient - : createTenantedApolloClient(appContainer, tenant.id) - const mutation = await client - .mutate({ - mutation: gql` - mutation DeleteTenant($id: String!) { - deleteTenant(id: $id) { - success - } + const mutation = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation DeleteTenant($id: String!) { + deleteTenant(id: $id) { + success } - `, - variables: { - id: tenant.id } - }) - .then( - (query): DeleteTenantMutationResponse => query.data?.deleteTenant - ) + `, + variables: { + id: tenant.id + } + }) + .then( + (query): DeleteTenantMutationResponse => query.data?.deleteTenant + ) - expect(mutation.success).toBe(true) - } - ) + expect(mutation.success).toBe(true) + }) - test('Cannot delete other tenant as non-operator', async (): Promise => { + test('Cannot delete tenant as non-operator', async (): Promise => { const firstTenant = await createTenant(deps) const secondTenant = await createTenant(deps) @@ -501,9 +491,9 @@ describe('Tenant Resolvers', (): void => { expect(error).toBeInstanceOf(ApolloError) expect((error as ApolloError).graphQLErrors).toContainEqual( expect.objectContaining({ - message: 'tenant does not exist', + message: 'permission denied', extensions: expect.objectContaining({ - code: GraphQLErrorCode.NotFound + code: GraphQLErrorCode.Forbidden }) }) ) diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index 6d27940a4e..19e86bd077 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -141,11 +141,11 @@ export const deleteTenant: MutationResolvers['deleteTenan args, ctx ): Promise => { - const { tenant: contextTenant, isOperator } = ctx - if (args.id !== contextTenant.id && !isOperator) { - throw new GraphQLError('tenant does not exist', { + const { isOperator } = ctx + if (!isOperator) { + throw new GraphQLError('permission denied', { extensions: { - code: GraphQLErrorCode.NotFound + code: GraphQLErrorCode.Forbidden } }) } From 8bc7d7f53b923751cdba38d0b53d6a3e2521a01e Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Thu, 30 Jan 2025 16:19:55 -0800 Subject: [PATCH 10/10] chore: cleanup --- packages/backend/jest.env.js | 2 +- packages/backend/scripts/init.sh | 2 -- packages/backend/src/graphql/resolvers/tenant.test.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/backend/jest.env.js b/packages/backend/jest.env.js index 8b804444ca..4a8435dd72 100644 --- a/packages/backend/jest.env.js +++ b/packages/backend/jest.env.js @@ -1,4 +1,4 @@ -process.env.LOG_LEVEL = 'info' +process.env.LOG_LEVEL = 'silent' process.env.INSTANCE_NAME = 'Rafiki' process.env.KEY_ID = 'myKey' process.env.OPEN_PAYMENTS_URL = 'http://127.0.0.1:3000' diff --git a/packages/backend/scripts/init.sh b/packages/backend/scripts/init.sh index 6f3b607aa9..a1fa255c7d 100755 --- a/packages/backend/scripts/init.sh +++ b/packages/backend/scripts/init.sh @@ -3,8 +3,6 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL DROP DATABASE IF EXISTS TESTING; - DROP DATABASE IF EXISTS TENANT_TESTING; CREATE DATABASE testing; - CREATE DATABASE tenant_testing; CREATE DATABASE development; EOSQL diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 628be01be6..177ed85015 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -99,7 +99,6 @@ describe('Tenant Resolvers', (): void => { ${false} | ${'tenant'} `('whoami query as $description', async ({ isOperator }): Promise => { const tenant = await createTenant(deps) - console.log('created tenant') const client = isOperator ? appContainer.apolloClient : createTenantedApolloClient(appContainer, tenant.id)