11import { OpaApiClient as Opa } from "./sdk/index.js" ;
2- import type { Input , Result } from "./sdk/models/components/index.js" ;
2+ import {
3+ type Input ,
4+ type Result ,
5+ type ResponsesSuccessfulPolicyResponse ,
6+ type ServerError ,
7+ BatchMixedResults ,
8+ BatchSuccessfulPolicyEvaluation ,
9+ SuccessfulPolicyResponse ,
10+ } from "./sdk/models/components/index.js" ;
311import {
412 ExecutePolicyWithInputResponse ,
513 ExecutePolicyResponse ,
614} from "./sdk/models/operations/index.js" ;
15+ import { SDKError } from "./sdk/models/errors/sdkerror.js" ;
16+ import { ServerError as ServerError_ } from "./sdk/models/errors/servererror.js" ;
717import { SDKOptions } from "./lib/config.js" ;
818import { HTTPClient } from "./lib/http.js" ;
919import { RequestOptions as FetchOptions } from "./lib/sdks.js" ;
@@ -37,12 +47,21 @@ export interface RequestOptions<Res> extends FetchOptions {
3747 fromResult ?: ( res ?: Result ) => Res ;
3848}
3949
50+ /** Extra per-request options for using the high-level SDK's
51+ * evaluateBatch method.
52+ */
53+ export interface BatchRequestOptions < Res > extends RequestOptions < Res > {
54+ rejectMixed ?: boolean ; // reject promise if the batch result is "mixed", i.e. if any of the items errored
55+ fallback ?: boolean ; // fall back to sequential evaluate calls if server doesn't support batch API
56+ }
57+
4058/** OPAClient is the starting point for using the high-level API.
4159 *
4260 * Use {@link Opa} if you need some low-level customization.
4361 */
4462export class OPAClient {
4563 private opa : Opa ;
64+ private opaFallback : boolean = false ;
4665
4766 /** Create a new `OPA` instance.
4867 * @param serverURL - The OPA URL, e.g. `https://opa.internal.corp:8443/`.
@@ -96,9 +115,10 @@ export class OPAClient {
96115 ) ;
97116 }
98117 if ( ! result . successfulPolicyResponse ) throw `no result in API response` ;
118+
99119 const res = result . successfulPolicyResponse . result ;
100- const fromResult = opts ?. fromResult ;
101- return fromResult ? fromResult ( res ) : ( res as Res ) ;
120+ const fromResult = opts ?. fromResult || id < Res > ;
121+ return fromResult ( res ) ;
102122 }
103123
104124 /** `evaluateDefault` is used to evaluate the server's default policy with optional input.
@@ -122,8 +142,126 @@ export class OPAClient {
122142 opts ,
123143 ) ;
124144 if ( ! resp . result ) throw `no result in API response` ;
125- const res = resp . result ;
126- const fromResult = opts ?. fromResult ;
127- return fromResult ? fromResult ( res ) : ( res as Res ) ;
145+
146+ const fromResult = opts ?. fromResult || id < Res > ;
147+ return fromResult ( resp . result ) ;
148+ }
149+
150+ /** `evaluateBatch` is used to evaluate the policy at the specified path, for a batch of many inputs.
151+ *
152+ * @param path - The path to the policy, without `/v1/batch/data`: use `authz/allow` to evaluate policy `data.authz.allow`.
153+ * @param inputs - The inputs to the policy.
154+ * @param opts - Per-request options to control how the policy evaluation result is to be transformed
155+ * into `Res` (via `fromResult`), if any failures in the batch result should reject the promose (via
156+ * `rejectMixed`), and low-level fetch options.
157+ */
158+ async evaluateBatch < In extends Input | ToInput , Res > (
159+ path : string ,
160+ inputs : { [ k : string ] : In } ,
161+ opts ?: BatchRequestOptions < Res > ,
162+ ) : Promise < { [ k : string ] : Res | ServerError } > {
163+ const inps = Object . fromEntries (
164+ Object . entries ( inputs ) . map ( ( [ k , inp ] ) => [
165+ k ,
166+ implementsToInput ( inp ) ? inp . toInput ( ) : inp ,
167+ ] ) ,
168+ ) ;
169+ let res : BatchMixedResults | BatchSuccessfulPolicyEvaluation | undefined ;
170+
171+ if ( this . opaFallback && opts ?. fallback ) {
172+ // memoized fallback: we have hit a 404 here before
173+ const responses = await this . fallbackBatch ( path , inps , opts ) ;
174+ res = { responses } ;
175+ } else {
176+ try {
177+ const resp = await this . opa . executeBatchPolicyWithInput (
178+ { path, requestBody : { inputs : inps } } ,
179+ opts ,
180+ ) ;
181+
182+ res = resp . batchMixedResults || resp . batchSuccessfulPolicyEvaluation ;
183+ } catch ( err ) {
184+ if (
185+ err instanceof SDKError &&
186+ err . httpMeta . response . status == 404 &&
187+ opts ?. fallback
188+ ) {
189+ this . opaFallback = true ;
190+ const responses = await this . fallbackBatch ( path , inps , opts ) ;
191+ res = { responses } ;
192+ } else {
193+ throw err ;
194+ }
195+ }
196+ }
197+
198+ if ( ! res ) throw `no result in API response` ;
199+
200+ const entries = [ ] ;
201+ for ( const [ k , v ] of Object . entries ( res ?. responses ?? { } ) ) {
202+ entries . push ( [ k , await processResult ( v , opts ) ] ) ;
203+ }
204+ return Object . fromEntries ( entries ) ;
128205 }
206+
207+ // run a sequence of evaluatePolicyWithInput(), via Promise.all/Promise.allSettled
208+ async fallbackBatch < Res > (
209+ path : string ,
210+ inputs : { [ k : string ] : Input } ,
211+ opts ?: BatchRequestOptions < Res > ,
212+ ) : Promise < { [ k : string ] : ServerError | SuccessfulPolicyResponse } > {
213+ let items : [ string , ServerError | SuccessfulPolicyResponse ] [ ] ;
214+ const keys = Object . keys ( inputs ) ;
215+ const ps = Object . values ( inputs ) . map ( ( input ) =>
216+ this . opa
217+ . executePolicyWithInput ( { path, requestBody : { input } } )
218+ . then ( ( { successfulPolicyResponse : res } ) => res ) ,
219+ ) ;
220+ if ( opts ?. rejectMixed ) {
221+ items = await Promise . all ( ps ) . then ( ( results ) =>
222+ results . map ( ( result , i ) => {
223+ if ( ! result ) throw `no result in API response` ;
224+ return [
225+ keys [ i ] as string , // can't be undefined
226+ result ,
227+ ] ;
228+ } ) ,
229+ ) ;
230+ } else {
231+ const settled = await Promise . allSettled ( ps ) . then ( ( results ) => {
232+ return results . map ( ( res , i ) => {
233+ if ( res . status === "rejected" ) {
234+ return [
235+ keys [ i ] ,
236+ {
237+ ...( res . reason as ServerError_ ) . data$ ,
238+ httpStatusCode : "500" ,
239+ } ,
240+ ] as [ string , ServerError ] ;
241+ }
242+ return [ keys [ i ] , res . value ] as [ string , SuccessfulPolicyResponse ] ;
243+ } ) ;
244+ } ) ;
245+ items = settled ;
246+ }
247+ return Object . fromEntries ( items ) ;
248+ }
249+ }
250+
251+ function processResult < Res > (
252+ res : ResponsesSuccessfulPolicyResponse | ServerError ,
253+ opts ?: BatchRequestOptions < Res > ,
254+ ) : Promise < Res | ServerError > {
255+ if ( res && "code" in res ) {
256+ if ( opts ?. rejectMixed ) return Promise . reject ( res as ServerError ) ;
257+
258+ return Promise . resolve ( res as ServerError ) ;
259+ }
260+
261+ const fromResult = opts ?. fromResult || id < Res > ;
262+ return Promise . resolve ( fromResult ( res . result ) ) ;
263+ }
264+
265+ function id < T > ( x : any ) : T {
266+ return x as T ;
129267}
0 commit comments