Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/deploy/functions/release/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@

await setupArtifactCleanupPolicies(
options,
options.projectId!,

Check warning on line 110 in src/deploy/functions/release/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
Object.keys(wantBackend.endpoints),
);

Expand Down Expand Up @@ -143,14 +143,15 @@
continue;
}
if (backend.isDataConnectGraphqlTriggered(httpsFunc)) {
const funcId = httpsFunc.id.toLowerCase().replaceAll("_", "-");
// The Cloud Functions backend only returns the non-deterministic hashed URL, which doesn't work for Data Connect
// as we do some verification against the project number and region in the URL, so we manually construct the deterministic URL.
// TODO: The deterministic URL is only available for DNS segments of 63 characters or less;
// we should add validation to prevent service names that would exceed this.
logger.info(
clc.bold("Function URL"),
`(${getFunctionLabel(httpsFunc)}):`,
`https://${httpsFunc.id.toLowerCase()}-${projectNumber}.${httpsFunc.region}.run.app`,
`https://${funcId}-${projectNumber}.${httpsFunc.region}.run.app`,
);
continue;
}
Expand Down
114 changes: 102 additions & 12 deletions src/init/features/dataconnect/resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as chai from "chai";
import * as clc from "colorette";
import * as fs from "fs-extra";
import * as yaml from "js-yaml";
import * as sinon from "sinon";

Expand All @@ -10,12 +9,15 @@
actuate,
ResolverRequiredInfo,
} from "./resolver";
import * as functions from "../functions";
import { Setup } from "../..";
import { Config } from "../../../config";
import * as load from "../../../dataconnect/load";
import { DataConnectYaml, ServiceInfo } from "../../../dataconnect/types";
import * as experiments from "../../../experiments";
import * as prompt from "../../../prompt";
import { Options } from "../../../options";
import { RC } from "../../../rc";

const expect = chai.expect;

Expand All @@ -33,6 +35,7 @@
id: "test_resolver",
uri: "www.test.com",
serviceInfo: {} as ServiceInfo,
shouldInitFunctions: false,
};
});

Expand Down Expand Up @@ -99,22 +102,38 @@
describe("askQuestions", () => {
let setup: Setup;
let config: Config;
let options: Options;
let experimentsStub: sinon.SinonStub;
let loadAllStub: sinon.SinonStub;
let selectStub: sinon.SinonStub;
let inputStub: sinon.SinonStub;
let confirmStub: sinon.SinonStub;
let functionsAskQuestionsStub: sinon.SinonStub;

beforeEach(() => {
setup = {
config: {} as any,

Check warning on line 115 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 115 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
rcfile: {} as any,

Check warning on line 116 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 116 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
instructions: [],
};
config = new Config({}, { projectDir: "." });
options = {
configPath: "",
only: "",
except: "",
filteredTargets: [],
force: false,
nonInteractive: false,
debug: false,
config: config,
rc: new RC(),
};
experimentsStub = sinon.stub(experiments, "isEnabled");
loadAllStub = sinon.stub(load, "loadAll");
selectStub = sinon.stub(prompt, "select");
inputStub = sinon.stub(prompt, "input");
confirmStub = sinon.stub(prompt, "confirm");
functionsAskQuestionsStub = sinon.stub(functions, "askQuestions").resolves();
});

afterEach(() => {
Expand All @@ -126,14 +145,15 @@
loadAllStub.resolves([]);

try {
await askQuestions(setup, config);
await askQuestions(setup, config, options);
} catch (err: any) {

Check warning on line 149 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
expect(err.message).to.equal(

Check warning on line 150 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .message on an `any` value
`No Firebase Data Connect workspace found. Run ${clc.bold(
"firebase init dataconnect",
)} to set up a service and main schema.`,
);
}
expect(functionsAskQuestionsStub.called).to.be.false;
});

it("should skip service selection when exactly one service", async () => {
Expand All @@ -145,8 +165,9 @@
},
]);
inputStub.onFirstCall().resolves("test_resolver");
confirmStub.resolves(true);

await askQuestions(setup, config);
await askQuestions(setup, config, options);

expect(selectStub.called).to.be.false;
expect(inputStub.calledOnce).to.be.true;
Expand All @@ -157,6 +178,7 @@
expect(setup.featureInfo?.dataconnectResolver?.serviceInfo.serviceName).to.equal(
"projects/project-id/locations/us-central1/services/service-id",
);
expect(functionsAskQuestionsStub.called).to.be.true;
});

it("should prompt for service selection when multiple services", async () => {
Expand All @@ -173,8 +195,9 @@
dataConnectYaml: { location: "us-central1", serviceId: "service-id2" },
});
inputStub.onFirstCall().resolves("test_resolver");
confirmStub.resolves(true);

await askQuestions(setup, config);
await askQuestions(setup, config, options);

expect(selectStub.calledOnce).to.be.true;
expect(inputStub.calledOnce).to.be.true;
Expand All @@ -185,6 +208,7 @@
expect(setup.featureInfo?.dataconnectResolver?.serviceInfo.serviceName).to.equal(
"projects/project-id/locations/us-central1/services/service-id2",
);
expect(functionsAskQuestionsStub.called).to.be.true;
});

it("uses project number in URI if set", async () => {
Expand All @@ -197,8 +221,35 @@
},
]);
inputStub.onFirstCall().resolves("test_resolver");
confirmStub.resolves(true);

await askQuestions(setup, config, options);

expect(selectStub.called).to.be.false;
expect(inputStub.calledOnce).to.be.true;
expect(setup.featureInfo?.dataconnectResolver?.id).to.equal("test_resolver");
expect(setup.featureInfo?.dataconnectResolver?.uri).to.equal(
"https://test_resolver-123456789.us-central1.run.app/graphql",
);
expect(setup.featureInfo?.dataconnectResolver?.serviceInfo.serviceName).to.equal(
"projects/project-id/locations/us-central1/services/service-id",
);
expect(functionsAskQuestionsStub.called).to.be.true;
});

it("skip functions init if not confirmed", async () => {
setup.projectNumber = "123456789";
experimentsStub.returns(true);
loadAllStub.resolves([
{
serviceName: "projects/project-id/locations/us-central1/services/service-id",
dataConnectYaml: { location: "us-central1", serviceId: "service-id" },
},
]);
inputStub.onFirstCall().resolves("test_resolver");
confirmStub.resolves(false);

await askQuestions(setup, config);
await askQuestions(setup, config, options);

expect(selectStub.called).to.be.false;
expect(inputStub.calledOnce).to.be.true;
Expand All @@ -209,6 +260,7 @@
expect(setup.featureInfo?.dataconnectResolver?.serviceInfo.serviceName).to.equal(
"projects/project-id/locations/us-central1/services/service-id",
);
expect(functionsAskQuestionsStub.called).to.be.false;
});
});

Expand All @@ -217,16 +269,16 @@
let config: Config;
let experimentsStub: sinon.SinonStub;
let writeProjectFileStub: sinon.SinonStub;
let ensureSyncStub: sinon.SinonStub;
let functionsActuateStub: sinon.SinonStub;

beforeEach(() => {
experimentsStub = sinon.stub(experiments, "isEnabled");
writeProjectFileStub = sinon.stub();
ensureSyncStub = sinon.stub(fs, "ensureFileSync");
functionsActuateStub = sinon.stub(functions, "actuate").resolves();

setup = {
config: { projectDir: "/path/to/project" } as any,

Check warning on line 280 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 280 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
rcfile: {} as any,

Check warning on line 281 in src/init/features/dataconnect/resolver.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
featureInfo: {
dataconnectResolver: {
id: "test_resolver",
Expand All @@ -248,6 +300,7 @@
},
connectorInfo: [],
},
shouldInitFunctions: false,
},
},
instructions: [],
Expand Down Expand Up @@ -277,22 +330,59 @@
await actuate(setup, config);

expect(writeProjectFileStub.called).to.be.false;
expect(ensureSyncStub.called).to.be.false;
expect(functionsActuateStub.called).to.be.false;
});

it("should write dataconnect.yaml and set up empty secondary schema file", async () => {
experimentsStub.returns(true);

await actuate(setup, config);

expect(writeProjectFileStub.calledOnce).to.be.true;
expect(writeProjectFileStub.calledTwice).to.be.true;
const writtenYamlPath = writeProjectFileStub.getCall(0).args[0];
const writtenYamlContents = writeProjectFileStub.getCall(0).args[1];
const parsedYaml = yaml.load(writtenYamlContents);
expect(writtenYamlPath).to.equal("../service/dataconnect.yaml");
expect(parsedYaml.schemas).to.have.lengthOf(2);
const writtenSchemaPath = writeProjectFileStub.getCall(1).args[0];
const writtenSchemaContents = writeProjectFileStub.getCall(1).args[1];
expect(writtenSchemaPath).to.equal("../service/schema_test_resolver/schema.gql");
expect(writtenSchemaContents).to.equal(`# Example Hello World custom resolver schema.

# Custom resolver fields can be defined on root Query and Mutation types.
type Query {
# This field will be backed by your Cloud Run function.
# Your "hello" function will take in a string argument "name" and return a string.
hello(name: String): String
}
`); // From SCHEMA_TEMPLATE
expect(functionsActuateStub.called).to.be.false;
});

it("should actuate functions", async () => {
experimentsStub.returns(true);
setup.featureInfo!.dataconnectResolver!.shouldInitFunctions = true;

await actuate(setup, config);

expect(writeProjectFileStub.calledTwice).to.be.true;
const writtenYamlPath = writeProjectFileStub.getCall(0).args[0];
const writtenYamlContents = writeProjectFileStub.getCall(0).args[1];
const parsedYaml = yaml.load(writtenYamlContents);
expect(writtenYamlPath).to.equal("../service/dataconnect.yaml");
expect(parsedYaml.schemas).to.have.lengthOf(2);
expect(ensureSyncStub.calledOnce).to.be.true;
const writtenSchemaPath = ensureSyncStub.getCall(0).args[0];
expect(writtenSchemaPath).to.equal("/path/to/service/schema_test_resolver/schema.gql");
const writtenSchemaPath = writeProjectFileStub.getCall(1).args[0];
const writtenSchemaContents = writeProjectFileStub.getCall(1).args[1];
expect(writtenSchemaPath).to.equal("../service/schema_test_resolver/schema.gql");
expect(writtenSchemaContents).to.equal(`# Example Hello World custom resolver schema.

# Custom resolver fields can be defined on root Query and Mutation types.
type Query {
# This field will be backed by your Cloud Run function.
# Your "hello" function will take in a string argument "name" and return a string.
hello(name: String): String
}
`); // From SCHEMA_TEMPLATE
expect(functionsActuateStub.called).to.be.true;
});
});
34 changes: 28 additions & 6 deletions src/init/features/dataconnect/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as clc from "colorette";
import * as fs from "fs-extra";
import { join, relative } from "path";
import * as yaml from "yaml";

import { input, select } from "../../../prompt";
import { confirm, input, select } from "../../../prompt";
import { Setup } from "../..";
import { logBullet, newUniqueId } from "../../../utils";
import { Config } from "../../../config";
Expand All @@ -14,18 +13,25 @@ import * as experiments from "../../../experiments";
import { isBillingEnabled } from "../../../gcp/cloudbilling";
import { trackGA4 } from "../../../track";
import { Source } from ".";
import * as functions from "../functions";
import { Options } from "../../../options";
import { readTemplateSync } from "../../../templates";

const SCHEMA_TEMPLATE = readTemplateSync("init/dataconnect/secondary_schema.gql");

export interface ResolverRequiredInfo {
id: string;
uri: string;
serviceInfo: ServiceInfo;
shouldInitFunctions: boolean;
}

export async function askQuestions(setup: Setup, config: Config): Promise<void> {
export async function askQuestions(setup: Setup, config: Config, options: Options): Promise<void> {
const resolverInfo: ResolverRequiredInfo = {
id: "",
uri: "",
serviceInfo: {} as ServiceInfo,
shouldInitFunctions: false,
};

const serviceInfos = await loadAll(setup.projectId || "", config);
Expand Down Expand Up @@ -66,8 +72,17 @@ export async function askQuestions(setup: Setup, config: Config): Promise<void>
" later.",
);

resolverInfo.shouldInitFunctions = await confirm({
message:
"Would you like to proceed with initializing a functions codebase with sample custom resolvers code?",
default: true,
});
setup.featureInfo = setup.featureInfo || {};
setup.featureInfo.dataconnectResolver = resolverInfo;

if (resolverInfo.shouldInitFunctions) {
await functions.askQuestions(setup, config, options);
}
}

export async function actuate(setup: Setup, config: Config) {
Expand Down Expand Up @@ -96,6 +111,9 @@ export async function actuate(setup: Setup, config: Config) {
Date.now() - startTime,
);
}
if (resolverInfo.shouldInitFunctions) {
await functions.actuate(setup, config);
}
}

function actuateWithInfo(config: Config, info: ResolverRequiredInfo) {
Expand All @@ -106,13 +124,17 @@ function actuateWithInfo(config: Config, info: ResolverRequiredInfo) {
info.serviceInfo.dataConnectYaml = dataConnectYaml;
const dataConnectYamlContents = yaml.stringify(dataConnectYaml);
const dataConnectYamlPath = join(info.serviceInfo.sourceDirectory, "dataconnect.yaml");
const dataConnectSchemaPath = join(
info.serviceInfo.sourceDirectory,
`schema_${info.id}`,
"schema.gql",
);
config.writeProjectFile(
relative(config.projectDir, dataConnectYamlPath),
dataConnectYamlContents,
);

// Write an empty schema.gql file.
fs.ensureFileSync(join(info.serviceInfo.sourceDirectory, `schema_${info.id}`, "schema.gql"));
// Write the schema.gql file pre-populated with a template.
config.writeProjectFile(relative(config.projectDir, dataConnectSchemaPath), SCHEMA_TEMPLATE);
}

/** Add secondary schema configuration to dataconnect.yaml in place */
Expand Down
Loading
Loading