Skip to content

Commit

Permalink
Support API URL configuration in JS and Python SDKs (#302)
Browse files Browse the repository at this point in the history
  • Loading branch information
ewanharris authored Feb 6, 2024
2 parents d9368c8 + abdcaba commit 1d2a6b0
Show file tree
Hide file tree
Showing 15 changed files with 350 additions and 106 deletions.
9 changes: 3 additions & 6 deletions config/clients/js/template/README_initializing.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ The documentation below refers to the `{{appShortName}}Client`, to read the docu
const { {{appShortName}}Client } = require('{{packageName}}'); // OR import { {{appShortName}}Client } from '{{packageName}}';

const fgaClient = new {{appShortName}}Client({
apiScheme: process.env.FGA_API_SCHEME, // optional, defaults to "https"
apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}})
apiUrl: process.env.FGA_API_URL, // required
storeId: process.env.FGA_STORE_ID, // not needed when calling `CreateStore` or `ListStores`
authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID, // Optional, can be overridden per request
});
Expand All @@ -21,8 +20,7 @@ const fgaClient = new {{appShortName}}Client({
const { {{appShortName}}Client } = require('{{packageName}}'); // OR import { {{appShortName}}Client } from '{{packageName}}';

const fgaClient = new {{appShortName}}Client({
apiScheme: process.env.FGA_API_SCHEME, // optional, defaults to "https"
apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}})
apiUrl: process.env.FGA_API_URL, // required
storeId: process.env.FGA_STORE_ID, // not needed when calling `CreateStore` or `ListStores`
authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID, // Optional, can be overridden per request
credentials: {
Expand All @@ -40,8 +38,7 @@ const fgaClient = new {{appShortName}}Client({
const { {{appShortName}}Client } = require('{{packageName}}'); // OR import { {{appShortName}}Client } from '{{packageName}}';

const fgaClient = new {{appShortName}}Client({
apiScheme: process.env.FGA_API_SCHEME, // optional, defaults to "https"
apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}})
apiUrl: process.env.FGA_API_URL, // required
storeId: process.env.FGA_STORE_ID, // not needed when calling `CreateStore` or `ListStores`
authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID, // Optional, can be overridden per request
credentials: {
Expand Down
39 changes: 34 additions & 5 deletions config/clients/js/template/configuration.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ export interface RetryParams {
}

export interface UserConfigurationParams {
apiUrl?: string;
/**
* @deprecated Replace usage of `apiScheme` + `apiHost` with `apiUrl`
*/
apiScheme?: string;
apiHost: string;
/**
* @deprecated Replace usage of `apiScheme` + `apiHost` with `apiUrl`
*/
apiHost?: string;
storeId?: string;
credentials?: CredentialsConfig;
baseOptions?: any;
Expand Down Expand Up @@ -58,18 +65,28 @@ export class Configuration {
*/
private static sdkVersion = "{{packageVersion}}";

/**
* provide the full api URL (e.g. `https://api.{{sampleApiDomain}}`)
*
* @type {string}
* @memberof Configuration
*/
apiUrl: string;

/**
* provide scheme (e.g. `https`)
*
* @type {string}
* @memberof Configuration
* @deprecated
*/
apiScheme = "https";
/**
* provide server host (e.g. `api.{{sampleApiDomain}}`)
*
* @type {string}
* @memberof Configuration
* @deprecated
*/
apiHost: string;
/**
Expand Down Expand Up @@ -104,6 +121,7 @@ export class Configuration {
constructor(params: UserConfigurationParams = {} as unknown as UserConfigurationParams) {
this.apiScheme = params.apiScheme || this.apiScheme;
this.apiHost = params.apiHost!;
this.apiUrl = params.apiUrl!;
this.storeId = params.storeId!;
const credentialParams = params.credentials;
Expand Down Expand Up @@ -156,12 +174,17 @@ export class Configuration {
* @throws {FgaValidationError}
*/
public isValid(): boolean {
assertParamExists("Configuration", "apiScheme", this.apiScheme);
assertParamExists("Configuration", "apiHost", this.apiHost);
if (!this.apiUrl) {
assertParamExists("Configuration", "apiScheme", this.apiScheme);
assertParamExists("Configuration", "apiHost", this.apiHost);
}

if (!isWellFormedUriString(this.getBasePath())) {
throw new FgaValidationError(
`Configuration.apiScheme (${this.apiScheme}) and Configuration.apiHost (${this.apiHost}) do not form a valid URI (${this.getBasePath()})`);
this.apiUrl ?
`Configuration.apiUrl (${this.apiUrl}) is not a valid URI (${this.getBasePath()})` :
`Configuration.apiScheme (${this.apiScheme}) and Configuration.apiHost (${this.apiHost}) do not form a valid URI (${this.getBasePath()})`
);
}

if (this.storeId && !isWellFormedUlidString(this.storeId)) {
Expand All @@ -178,5 +201,11 @@ export class Configuration {
/**
* Returns the API base path (apiScheme+apiHost)
*/
public getBasePath: () => string = () => `${this.apiScheme}://${this.apiHost}`;
public getBasePath: () => string = () => {
if (this.apiUrl) {
return this.apiUrl
} else {
return `${this.apiScheme}://${this.apiHost}`
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Configuration, UserConfigurationParams } from "../../configuration";
import { CredentialsMethod } from "../../credentials";

export const {{appUpperCaseName}}_STORE_ID = "01H0H015178Y2V4CX10C2KGHF4";
export const {{appUpperCaseName}}_API_HOST = "api.{{sampleApiDomain}}";
export const {{appUpperCaseName}}_API_URL = "http://api.{{sampleApiDomain}}";
export const {{appUpperCaseName}}_API_TOKEN_ISSUER = "tokenissuer.{{sampleApiDomain}}";
export const {{appUpperCaseName}}_API_AUDIENCE = "https://api.{{sampleApiDomain}}/";
export const {{appUpperCaseName}}_CLIENT_ID = "01H0H3D8TD07EWAQHXY9BWJG3V";
Expand All @@ -13,7 +13,7 @@ export const {{appUpperCaseName}}_API_TOKEN = "fga_abcdef";

export const baseConfig: UserConfigurationParams = {
storeId: {{appUpperCaseName}}_STORE_ID,
apiHost: {{appUpperCaseName}}_API_HOST,
apiUrl: {{appUpperCaseName}}_API_URL,
credentials: {
method: CredentialsMethod.ClientCredentials,
config: {
Expand Down
26 changes: 19 additions & 7 deletions config/clients/js/template/tests/index.test.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AuthCredentialsConfig } from "../credentials";
import {
baseConfig,
defaultConfiguration,
{{appUpperCaseName}}_API_HOST,
{{appUpperCaseName}}_API_URL,
{{appUpperCaseName}}_API_TOKEN_ISSUER,
{{appUpperCaseName}}_STORE_ID
} from "./helpers/default-config";
Expand Down Expand Up @@ -53,16 +53,28 @@ describe("{{appTitleCaseName}} SDK", function () {

it("should require host in configuration", () => {
expect(
() => new {{appShortName}}Api({ ...baseConfig, apiHost: undefined! })
() => new {{appShortName}}Api({ ...baseConfig, apiUrl: undefined! })
).toThrowError();
});

it("should validate host in configuration (adding scheme as part of the host)", () => {
expect(
() => new {{appShortName}}Api({ ...baseConfig, apiHost: "https://api.{{sampleApiDomain}}" })
() => new {{appShortName}}Api({ ...baseConfig, apiUrl: "//api.{{sampleApiDomain}}" })
).toThrowError();
});

it("should allow using apiHost if apiUrl is not provided", () => {
expect(
() => new OpenFgaApi({ ...baseConfig, apiHost: "api.fga.example" })
).not.toThrowError();
});

it("should still validate apiHost", () => {
expect(
() => new OpenFgaApi({ ...baseConfig, apiHost: "//api.{{sampleApiDomain}}" })
).not.toThrowError();
});

it("should validate apiTokenIssuer in configuration (should not allow scheme as part of the apiTokenIssuer)", () => {
expect(
() => new {{appShortName}}Api({
Expand All @@ -83,7 +95,7 @@ describe("{{appTitleCaseName}} SDK", function () {
() =>
new {{appShortName}}Api({
storeId: baseConfig.storeId!,
apiHost: baseConfig.apiHost,
apiUrl: baseConfig.apiUrl,
})
).not.toThrowError();
});
Expand All @@ -93,7 +105,7 @@ describe("{{appTitleCaseName}} SDK", function () {
() =>
new {{appShortName}}Api({
storeId: baseConfig.storeId!,
apiHost: baseConfig.apiHost,
apiUrl: baseConfig.apiUrl,
credentials: {
method: CredentialsMethod.ApiToken as any
}
Expand Down Expand Up @@ -229,7 +241,7 @@ describe("{{appTitleCaseName}} SDK", function () {

const fgaApi = new {{appShortName}}Api({
storeId: baseConfig.storeId!,
apiHost: baseConfig.apiHost,
apiUrl: baseConfig.apiUrl,
});
expect(scope.isDone()).toBe(false);

Expand All @@ -247,7 +259,7 @@ describe("{{appTitleCaseName}} SDK", function () {

it("should allow updating the storeId after initialization", async () => {
const fgaApi = new {{appShortName}}Api({
apiHost: {{appUpperCaseName}}_API_HOST
apiUrl: {{appUpperCaseName}}_API_URL
});
expect(fgaApi.storeId).toBe(undefined);
fgaApi.storeId = {{appUpperCaseName}}_STORE_ID;
Expand Down
Loading

0 comments on commit 1d2a6b0

Please sign in to comment.