Skip to content

Commit b1f1cbd

Browse files
authored
Update use of schema API for the pull command (#499)
* Update use of schema API for pull command
1 parent 42d02d6 commit b1f1cbd

File tree

4 files changed

+140
-80
lines changed

4 files changed

+140
-80
lines changed

src/commands/schema/diff.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { reformatFSL } from "../../lib/schema.mjs";
1212
import { localSchemaOptions } from "./schema.mjs";
1313

1414
/**
15-
* @returns string[]
15+
* @returns {[string, string]} An tuple containing the source and target schema
1616
*/
1717
function parseTarget(argv) {
1818
if (!argv.active && !argv.staged) {

src/commands/schema/pull.mjs

+34-29
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
//@ts-check
22

33
import { container } from "../../cli.mjs";
4-
import {
5-
CommandError,
6-
yargsWithCommonQueryOptions,
7-
} from "../../lib/command-helpers.mjs";
4+
import { yargsWithCommonQueryOptions } from "../../lib/command-helpers.mjs";
85
import { getSecret } from "../../lib/fauna-client.mjs";
96
import { localSchemaOptions } from "./schema.mjs";
107

@@ -36,9 +33,9 @@ async function determineFileState(argv, filenames) {
3633
return { adds, deletes, existing, overwrites };
3734
}
3835

39-
function logDiff({ argv, adds, overwrites, deletes }) {
36+
function logDiff({ argv, adds, overwrites, deletes, source }) {
4037
const logger = container.resolve("logger");
41-
logger.stdout("Pull will make the following changes:");
38+
logger.stdout(`Pulling ${source} schema will make the following changes:`);
4239
if (argv.delete) {
4340
for (const deleteme of deletes) {
4441
logger.stdout(`delete: ${deleteme}`);
@@ -58,10 +55,29 @@ async function doPull(argv) {
5855
const makeFaunaRequest = container.resolve("makeFaunaRequest");
5956
const secret = await getSecret();
6057

58+
// Get the staged schema status
59+
/** @type {{ status: "none" | "pending" | "ready" | "failed", version: string }} */
60+
const statusResponse = await makeFaunaRequest({
61+
argv,
62+
path: "/schema/1/staged/status",
63+
method: "GET",
64+
secret,
65+
});
66+
67+
const version = statusResponse.version;
68+
const source =
69+
argv.active || statusResponse.status === "none" ? "active" : "staged";
70+
71+
const filesParams = new URLSearchParams({
72+
version,
73+
staged: source === "staged" ? "true" : "false",
74+
});
75+
6176
// fetch the list of remote FSL files
6277
const filesResponse = await makeFaunaRequest({
6378
argv,
6479
path: "/schema/1/files",
80+
params: filesParams,
6581
method: "GET",
6682
secret,
6783
});
@@ -72,31 +88,16 @@ async function doPull(argv) {
7288
.filter((name) => name.endsWith(".fsl"))
7389
.sort();
7490

75-
// check if there's a staged schema
76-
const statusResponse = await makeFaunaRequest({
77-
argv,
78-
path: "/schema/1/staged/status",
79-
params: new URLSearchParams({ version: filesResponse.version }),
80-
method: "GET",
81-
secret,
82-
});
83-
84-
// if there's a staged schema, cannot use the --active flag.
85-
// getting active FSL while staged FSL exists is not yet
86-
// implemented at the service level.
87-
if (statusResponse.status !== "none" && argv.active) {
88-
throw new CommandError(
89-
"There is a staged schema change. Remove the --active flag to pull it.",
90-
);
91-
} else if (statusResponse.status === "none" && !argv.active) {
92-
throw new CommandError("There are no staged schema changes to pull.");
93-
}
91+
logger.debug(
92+
`Pulling remote ${source} schema, version '${version}'.`,
93+
"schema-pull",
94+
);
9495

9596
const { adds, deletes, overwrites } = await determineFileState(
9697
argv,
9798
filenames,
9899
);
99-
logDiff({ argv, adds, deletes, overwrites });
100+
logDiff({ argv, adds, deletes, overwrites, source });
100101

101102
const confirmed = await confirm({
102103
message: "Accept the changes?",
@@ -108,7 +109,12 @@ async function doPull(argv) {
108109
const getAllSchemaFileContents = container.resolve(
109110
"getAllSchemaFileContents",
110111
);
111-
const contents = await getAllSchemaFileContents(filenames, argv);
112+
const contents = await getAllSchemaFileContents(
113+
filenames,
114+
source,
115+
version,
116+
argv,
117+
);
112118

113119
// don't start writing or deleting files until we've successfully fetched all
114120
// the remote schema files
@@ -138,8 +144,7 @@ function buildPullCommand(yargs) {
138144
default: false,
139145
},
140146
active: {
141-
description:
142-
"Pull the database's active schema files. If omitted, pulls the database's staged schema, if available.",
147+
description: "Pull the database's active schema files.",
143148
type: "boolean",
144149
default: false,
145150
},

src/lib/schema.mjs

+15-1
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,33 @@ export async function writeSchemaFiles(dir, filenameToContentsDict) {
167167

168168
/**
169169
* @param {string[]} filenames - A list of schema file names to fetch
170+
* @param {"active" | "staged"} source - The source to pull from
171+
* @param {string} version - The schema version for optimistic concurrency control
170172
* @param {object} argv
171173
* @returns {Promise<Record<string, string>>} A map of schema file names to their contents.
172174
*/
173-
export async function getAllSchemaFileContents(filenames, argv) {
175+
export async function getAllSchemaFileContents(
176+
filenames,
177+
source,
178+
version,
179+
argv,
180+
) {
174181
const promises = [];
175182
/** @type Record<string, string> */
176183
const fileContentCollection = {};
177184
const secret = await getSecret();
185+
186+
const params = new URLSearchParams({
187+
version: version,
188+
staged: source === "staged" ? "true" : "false",
189+
});
190+
178191
for (const filename of filenames) {
179192
promises.push(
180193
makeFaunaRequest({
181194
argv,
182195
path: `/schema/1/files/${encodeURIComponent(filename)}`,
196+
params,
183197
method: "GET",
184198
secret,
185199
}).then(({ content }) => {

test/schema/pull.mjs

+90-49
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import * as awilix from "awilix";
44
import { expect } from "chai";
55
import sinon from "sinon";
6-
import tryToCatch from "try-to-catch";
76

87
import { run } from "../../src/cli.mjs";
98
import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.mjs";
@@ -39,7 +38,7 @@ describe("schema pull", function () {
3938
gatherFSL = container.resolve("gatherFSL");
4039
});
4140

42-
it("can pull schema, adding new files and overwriting existing files", async function () {
41+
it("can pull schema, adding new files and overwriting existing files", async function () {
4342
gatherFSL.resolves([
4443
{
4544
name: "coll.fsl",
@@ -59,17 +58,18 @@ describe("schema pull", function () {
5958
fetch.onCall(0).resolves(
6059
f({
6160
version: "194838274939473",
62-
files: [
63-
{ filename: "main.fsl" },
64-
{ filename: "second.fsl" },
65-
{ filename: "third.fsl" },
66-
],
61+
// assume no staged schema for this test
62+
status: "none",
6763
}),
6864
);
6965
fetch.onCall(1).resolves(
7066
f({
7167
version: "194838274939473",
72-
status: "ready",
68+
files: [
69+
{ filename: "main.fsl" },
70+
{ filename: "second.fsl" },
71+
{ filename: "third.fsl" },
72+
],
7373
}),
7474
);
7575
fetch.onCall(2).resolves(
@@ -98,35 +98,44 @@ describe("schema pull", function () {
9898
expect(gatherFSL).to.have.been.calledWith(".");
9999

100100
expect(fetch).to.have.been.calledWith(
101-
buildUrl("/schema/1/files"),
101+
buildUrl("/schema/1/staged/status"),
102102
commonFetchParams,
103103
);
104104
// the version param in the URL is important - we use it for optimistic locking
105105
expect(fetch).to.have.been.calledWith(
106-
buildUrl("/schema/1/staged/status", {
106+
buildUrl("/schema/1/files", {
107107
version: "194838274939473",
108-
color: "ansi",
108+
staged: "false",
109109
}),
110110
commonFetchParams,
111111
);
112112
expect(fetch).to.have.been.calledWith(
113-
buildUrl("/schema/1/files/main.fsl"),
113+
buildUrl("/schema/1/files/main.fsl", {
114+
version: "194838274939473",
115+
staged: "false",
116+
}),
114117
commonFetchParams,
115118
);
116119
expect(fetch).to.have.been.calledWith(
117-
buildUrl("/schema/1/files/second.fsl"),
120+
buildUrl("/schema/1/files/second.fsl", {
121+
version: "194838274939473",
122+
staged: "false",
123+
}),
118124
commonFetchParams,
119125
);
120126
expect(fetch).to.have.been.calledWith(
121-
buildUrl("/schema/1/files/third.fsl"),
127+
buildUrl("/schema/1/files/third.fsl", {
128+
version: "194838274939473",
129+
staged: "false",
130+
}),
122131
commonFetchParams,
123132
);
124133

125134
expect(logger.stdout).to.have.been.calledWith("overwrite: main.fsl");
126135
expect(logger.stdout).to.have.been.calledWith("add: second.fsl");
127136
expect(logger.stdout).to.have.been.calledWith("add: third.fsl");
128137
expect(logger.stdout).to.have.been.calledWith(
129-
"Pull will make the following changes:",
138+
"Pulling active schema will make the following changes:",
130139
);
131140

132141
expect(fs.mkdirSync).to.have.been.calledWith(".", { recursive: true });
@@ -168,17 +177,18 @@ describe("schema pull", function () {
168177
fetch.onCall(0).resolves(
169178
f({
170179
version: "194838274939473",
171-
files: [
172-
{ filename: "main.fsl" },
173-
{ filename: "second.fsl" },
174-
{ filename: "third.fsl" },
175-
],
180+
// assume no staged schema for this test
181+
status: "none",
176182
}),
177183
);
178184
fetch.onCall(1).resolves(
179185
f({
180186
version: "194838274939473",
181-
status: "ready",
187+
files: [
188+
{ filename: "main.fsl" },
189+
{ filename: "second.fsl" },
190+
{ filename: "third.fsl" },
191+
],
182192
}),
183193
);
184194

@@ -189,7 +199,7 @@ describe("schema pull", function () {
189199
expect(logger.stdout).to.have.been.calledWith("add: third.fsl");
190200
expect(logger.stdout).to.have.been.calledWith("delete: coll.fsl");
191201
expect(logger.stdout).to.have.been.calledWith(
192-
"Pull will make the following changes:",
202+
"Pulling active schema will make the following changes:",
193203
);
194204
expect(logger.stdout).to.have.been.calledWith("Change cancelled.");
195205
expect(fs.writeFile).to.have.not.been.called;
@@ -215,13 +225,14 @@ describe("schema pull", function () {
215225
fetch.onCall(0).resolves(
216226
f({
217227
version: "194838274939473",
218-
files: [{ filename: "main.fsl" }],
228+
// assume no staged schema for this test
229+
status: "none",
219230
}),
220231
);
221232
fetch.onCall(1).resolves(
222233
f({
223234
version: "194838274939473",
224-
status: "ready",
235+
files: [{ filename: "main.fsl" }],
225236
}),
226237
);
227238
fetch.onCall(2).resolves(
@@ -241,7 +252,7 @@ describe("schema pull", function () {
241252
expect(logger.stdout).to.have.been.calledWith("overwrite: main.fsl");
242253
expect(logger.stdout).to.have.been.calledWith("delete: coll.fsl");
243254
expect(logger.stdout).to.have.been.calledWith(
244-
"Pull will make the following changes:",
255+
"Pulling active schema will make the following changes:",
245256
);
246257

247258
expect(fs.mkdirSync).to.have.been.calledWith(".", { recursive: true });
@@ -255,30 +266,60 @@ describe("schema pull", function () {
255266

256267
it.skip("does not modify the filesystem if it fails to read file contents", async function () {});
257268

258-
it("errors if called with the --active flag while a schema change is staged", async function () {
259-
fetch.onCall(0).resolves(
260-
f({
261-
version: "194838274939473",
262-
files: [
263-
{ filename: "main.fsl" },
264-
{ filename: "second.fsl" },
265-
{ filename: "third.fsl" },
266-
],
267-
}),
268-
);
269-
fetch.onCall(1).resolves(
270-
f({
271-
version: "194838274939473",
272-
status: "ready",
273-
}),
274-
);
269+
[
270+
{ argvActive: true, remoteStaged: true, expectedStagedParam: "false" },
271+
{ argvActive: false, remoteStaged: false, expectedStagedParam: "false" },
272+
{ argvActive: false, remoteStaged: true, expectedStagedParam: "true" },
273+
].forEach(({ argvActive, remoteStaged, expectedStagedParam }) => {
274+
it(`can pull ${expectedStagedParam === "true" ? "staged" : "active"} schema while schema is ${remoteStaged ? "staged" : "not staged"}, and --active is ${argvActive ? "specified" : "not specified"}`, async function () {
275+
fetch.onCall(0).resolves(
276+
f({
277+
version: "194838274939473",
278+
status: remoteStaged ? "ready" : "none",
279+
}),
280+
);
281+
fetch.onCall(1).resolves(
282+
f({
283+
version: "194838274939473",
284+
files: [{ filename: "main.fsl" }],
285+
}),
286+
);
287+
fetch.onCall(2).resolves(
288+
f({
289+
content:
290+
"collection Main {\\n name: String\\n index byName {\\n terms [.name]\\n }\\n}\\n",
291+
}),
292+
);
275293

276-
const [error] = await tryToCatch(() =>
277-
run(`schema pull --secret "secret" --active`, container),
278-
);
279-
expect(error).to.have.property("code", 1);
280-
expect(container.resolve("gatherFSL")).to.not.have.been.called;
281-
});
294+
// user accepts the changes in the interactive prompt
295+
confirm.resolves(true);
296+
297+
await run(
298+
`schema pull --secret "secret" ${argvActive ? "--active" : ""}`,
299+
container,
300+
);
282301

283-
it.skip("errors if there are no staged schema changes to pull");
302+
expect(fetch).to.have.been.calledWith(
303+
buildUrl("/schema/1/staged/status"),
304+
commonFetchParams,
305+
);
306+
// the version param in the URL is important - we use it for optimistic locking
307+
expect(fetch).to.have.been.calledWith(
308+
buildUrl("/schema/1/files", {
309+
version: "194838274939473",
310+
staged: expectedStagedParam,
311+
}),
312+
commonFetchParams,
313+
);
314+
expect(fetch).to.have.been.calledWith(
315+
buildUrl("/schema/1/files/main.fsl", {
316+
version: "194838274939473",
317+
staged: expectedStagedParam,
318+
}),
319+
commonFetchParams,
320+
);
321+
322+
expect(logger.stderr).to.not.have.been.called;
323+
});
324+
});
284325
});

0 commit comments

Comments
 (0)