mirror: add public method `extract` (#894)

Summary:
The `extract` method lets you get data out of a mirror in a structured
format.

The mirror module now contains all the plumbing needed to provide
meaningful value. Remaining to be implemented are some internal
porcelain and a public method to perform an update step.

This makes progress toward #622.

Test Plan:
Comprehensive unit tests included, with full coverage; run `yarn unit`.

wchargin-branch: mirror-extract
This commit is contained in:
William Chargin 2018-09-26 12:04:10 -07:00 committed by GitHub
parent 90ace93f91
commit b74f1f3714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 1015 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import type Database, {BindingDictionary, Statement} from "better-sqlite3";
import stringify from "json-stable-stringify"; import stringify from "json-stable-stringify";
import dedent from "../util/dedent"; import dedent from "../util/dedent";
import * as NullUtil from "../util/null";
import * as Schema from "./schema"; import * as Schema from "./schema";
import * as Queries from "./queries"; import * as Queries from "./queries";
@ -945,6 +946,269 @@ export class Mirror {
// Last-updates, primitives, and links all updated: we're done. // Last-updates, primitives, and links all updated: we're done.
} }
/**
* Extract a structured object and all of its transitive dependencies
* from the database.
*
* The result is an object whose keys are fieldnames, and whose values
* are:
*
* - for the ID field: the object ID;
* - for primitive fields: the corresponding primitive value;
* - for node reference fields: a reference to the corresponding
* extracted object, which may be `null`;
* - for connection fields: an in-order array of the corresponding
* extracted objects, each of which may be `null`.
*
* For instance, the result of `extract("issue:1")` might be:
*
* {
* id: "issue:1172",
* title: "bug: holding <Space> causes CPU to overheat",
* body: "We should fix this immediately.",
* author: {
* id: "user:admin",
* login: "admin",
* },
* comments: [
* {
* body: "I depend on this behavior; please do not change it.",
* author: {
* id: "user:longtimeuser4",
* login: "longtimeuser4",
* },
* },
* {
* body: "That's horrifying.",
* author: {
* id: "user:admin",
* login: "admin",
* },
* },
* ],
* }
*
* The returned structure may be circular.
*
* If a node appears more than one time in the result---for instance,
* the "user:admin" node above---all instances will refer to the same
* object. However, objects are distinct across calls to `extract`, so
* it is safe to deeply mutate the result of this function.
*
* The provided object ID must correspond to a known object, or an
* error will be thrown. Furthermore, all transitive dependencies of
* the object must have been at least partially loaded at some point,
* or an error will be thrown.
*/
extract(rootId: Schema.ObjectId): mixed {
const db = this._db;
return _inTransaction(db, () => {
// We'll compute the transitive dependencies and store them into a
// temporary table. To do so, we first find a free table name.
const temporaryTableName: string = _nontransactionallyFindUnusedTableName(
db,
"tmp_transitive_dependencies_"
);
db.prepare(
`CREATE TEMPORARY TABLE ${temporaryTableName} ` +
"(id TEXT NOT NULL PRIMARY KEY, typename TEXT NOT NULL)"
).run();
try {
db.prepare(
dedent`\
WITH RECURSIVE
direct_dependencies (parent_id, child_id) AS (
SELECT parent_id, child_id FROM links
WHERE child_id IS NOT NULL
UNION
SELECT DISTINCT
connections.object_id AS parent_id,
connection_entries.child_id AS child_id
FROM connections JOIN connection_entries
ON connections.rowid = connection_entries.connection_id
WHERE child_id IS NOT NULL
),
transitive_dependencies (id) AS (
VALUES (:rootId) UNION
SELECT direct_dependencies.child_id
FROM transitive_dependencies JOIN direct_dependencies
ON transitive_dependencies.id = direct_dependencies.parent_id
)
INSERT INTO ${temporaryTableName} (id, typename)
SELECT objects.id AS id, objects.typename AS typename
FROM objects JOIN transitive_dependencies
ON objects.id = transitive_dependencies.id
`
).run({rootId});
const typenames: $ReadOnlyArray<Schema.Typename> = db
.prepare(`SELECT DISTINCT typename FROM ${temporaryTableName}`)
.pluck()
.all();
// Check to make sure all required objects and connections have
// been updated at least once.
{
const neverUpdatedEntry: void | {|
+id: Schema.ObjectId,
+fieldname: null | Schema.Fieldname,
|} = db
.prepare(
dedent`\
SELECT objects.id AS id, NULL as fieldname
FROM ${temporaryTableName}
JOIN objects USING (id)
WHERE objects.last_update IS NULL
UNION ALL
SELECT objects.id AS id, connections.fieldname AS fieldname
FROM ${temporaryTableName}
JOIN objects
USING (id)
LEFT OUTER JOIN connections
ON objects.id = connections.object_id
WHERE
connections.rowid IS NOT NULL
AND connections.last_update IS NULL
`
)
.get();
if (neverUpdatedEntry !== undefined) {
const entry = neverUpdatedEntry;
const s = JSON.stringify;
const missingData: string =
entry.fieldname == null
? "own data"
: `${s(entry.fieldname)} connection`;
throw new Error(
`${s(rootId)} transitively depends on ${s(entry.id)}, ` +
`but that object's ${missingData} has never been fetched`
);
}
}
// Constructing the result set inherently requires mutation,
// because the object graph can have cycles. We start by
// creating a record for each object, with just that object's
// primitive data. Then, we link in node references and
// connection entries.
const allObjects: Map<Schema.ObjectId, Object> = new Map();
for (const typename of typenames) {
const objectType = this._schemaInfo.objectTypes[typename];
// istanbul ignore if: should not be possible using the
// publicly accessible APIs
if (objectType == null) {
throw new Error(
`Corruption: unknown object type ${JSON.stringify(typename)}`
);
}
const primitivesTableName = _primitivesTableName(typename);
const selections = [
`${primitivesTableName}.id AS id`,
...objectType.primitiveFieldNames.map(
(fieldname) =>
`${primitivesTableName}."${fieldname}" AS "${fieldname}"`
),
].join(", ");
const rows: $ReadOnlyArray<{|
+id: Schema.ObjectId,
+[Schema.Fieldname]: string,
|}> = db
.prepare(
dedent`\
SELECT ${selections}
FROM ${temporaryTableName} JOIN ${primitivesTableName}
USING (id)
`
)
.all();
for (const row of rows) {
const object = {};
object.id = row.id;
for (const key of Object.keys(row)) {
if (key === "id") continue;
object[key] = JSON.parse(row[key]);
}
allObjects.set(object.id, object);
}
}
// Add links.
{
const getLinks = db.prepare(
dedent`\
SELECT
parent_id AS parentId,
fieldname AS fieldname,
child_id AS childId
FROM ${temporaryTableName} JOIN links
ON ${temporaryTableName}.id = links.parent_id
`
);
for (const link: {|
+parentId: Schema.ObjectId,
+fieldname: Schema.Fieldname,
+childId: Schema.ObjectId | null,
|} of getLinks.iterate()) {
const parent = NullUtil.get(allObjects.get(link.parentId));
const child =
link.childId == null
? null
: NullUtil.get(allObjects.get(link.childId));
parent[link.fieldname] = child;
}
}
// Add connections.
{
const getConnectionData = db.prepare(
dedent`\
SELECT
objects.id AS parentId,
connections.fieldname AS fieldname,
connection_entries.connection_id IS NOT NULL AS hasContents,
connection_entries.child_id AS childId
FROM ${temporaryTableName}
JOIN objects
USING (id)
JOIN connections
ON objects.id = connections.object_id
LEFT OUTER JOIN connection_entries
ON connections.rowid = connection_entries.connection_id
ORDER BY
objects.id, connections.fieldname, connection_entries.idx ASC
`
);
for (const datum: {|
+parentId: Schema.ObjectId,
+fieldname: Schema.Fieldname,
+hasContents: 0 | 1,
+childId: Schema.ObjectId | null,
|} of getConnectionData.iterate()) {
const parent = NullUtil.get(allObjects.get(datum.parentId));
if (parent[datum.fieldname] === undefined) {
parent[datum.fieldname] = [];
}
if (datum.hasContents) {
const child =
datum.childId == null
? null
: NullUtil.get(allObjects.get(datum.childId));
parent[datum.fieldname].push(child);
}
}
}
const result = allObjects.get(rootId);
if (result === undefined) {
throw new Error("No such object: " + JSON.stringify(rootId));
}
return result;
} finally {
this._db.prepare(`DROP TABLE ${temporaryTableName}`).run();
}
});
}
} }
/** /**

View File

@ -1378,6 +1378,757 @@ describe("graphql/mirror", () => {
expect(format([query])).toMatchSnapshot(); expect(format([query])).toMatchSnapshot();
}); });
}); });
describe("extract", () => {
// A schema with some useful edge cases.
function buildTestSchema(): Schema.Schema {
const s = Schema;
return s.schema({
Caveman: s.object({
id: s.id(),
only: s.primitive(),
primitives: s.primitive(),
}),
Feline: s.object({
id: s.id(),
only: s.node("Feline"),
lynx: s.node("Feline"),
}),
Socket: s.object({
id: s.id(),
only: s.connection("Socket"),
connections: s.connection("Socket"),
}),
Empty: s.object({
id: s.id(),
}),
});
}
type Caveman = {|
+id: string,
+only: mixed,
+primitives: mixed,
|};
type Feline = {|
+id: string,
+only: null | Feline,
+lynx: null | Feline,
|};
type Socket = {|
+id: string,
+only: $ReadOnlyArray<null | Socket>,
+connections: $ReadOnlyArray<null | Socket>,
|};
it("fails if the provided object does not exist", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
expect(() => {
mirror.extract("wat");
}).toThrow('No such object: "wat"');
});
it("fails if the provided object is missing own-data", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Caveman", id: "brog"});
expect(() => {
mirror.extract("brog");
}).toThrow(
'"brog" transitively depends on "brog", ' +
"but that object's own data has never been fetched"
);
});
it("fails if the provided object is missing connection data", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Socket", id: "localhost"});
mirror.registerObject({typename: "Socket", id: "loopback"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Socket", id: "localhost"},
{__typename: "Socket", id: "loopback"},
]);
mirror._updateConnection(updateId, "localhost", "connections", {
totalCount: 0,
pageInfo: {hasNextPage: false, endCursor: null},
nodes: [],
});
expect(() => {
mirror.extract("localhost");
}).toThrow(
'"localhost" transitively depends on "localhost", ' +
'but that object\'s "only" connection has never been fetched'
);
mirror._updateConnection(updateId, "loopback", "only", {
totalCount: 0,
pageInfo: {hasNextPage: false, endCursor: null},
nodes: [],
});
expect(() => {
mirror.extract("loopback");
}).toThrow(
'"loopback" transitively depends on "loopback", ' +
'but that object\'s "connections" connection has never been fetched'
);
});
it("fails if a transitive dependency is missing own-data", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Feline", id: "alpha"});
mirror.registerObject({typename: "Feline", id: "beta"});
mirror.registerObject({typename: "Feline", id: "gamma"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{
__typename: "Feline",
id: "alpha",
only: null,
lynx: {__typename: "Feline", id: "beta"},
},
{
__typename: "Feline",
id: "beta",
only: null,
lynx: {__typename: "Feline", id: "gamma"},
},
]);
expect(() => {
mirror.extract("alpha");
}).toThrow(
'"alpha" transitively depends on "gamma", ' +
"but that object's own data has never been fetched"
);
});
it("fails if a transitive dependency is missing connection data", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Socket", id: "localhost:8080"});
mirror.registerObject({typename: "Socket", id: "localhost:7070"});
mirror.registerObject({typename: "Socket", id: "localhost:6060"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Socket", id: "localhost:8080"},
{__typename: "Socket", id: "localhost:7070"},
{__typename: "Socket", id: "localhost:6060"},
]);
const updateConnection = (
objectId: Schema.ObjectId,
fieldname: Schema.Fieldname,
ids: $ReadOnlyArray<Schema.ObjectId>
) => {
mirror._updateConnection(updateId, objectId, fieldname, {
totalCount: ids.length,
pageInfo: {hasNextPage: false, endCursor: String(ids.length)},
nodes: ids.map((id) => ({__typename: "Socket", id})),
});
};
updateConnection("localhost:8080", "only", []);
updateConnection("localhost:7070", "only", []);
updateConnection("localhost:6060", "only", []);
updateConnection("localhost:8080", "connections", ["localhost:7070"]);
updateConnection("localhost:7070", "connections", ["localhost:6060"]);
expect(() => {
mirror.extract("localhost:8080");
}).toThrow(
'"localhost:8080" transitively depends on "localhost:6060", ' +
'but that object\'s "connections" connection has never been fetched'
);
});
it("handles objects that only have primitive fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Caveman", id: "brog"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Caveman", id: "brog", only: "ugg", primitives: "ook"},
]);
const result: Caveman = (mirror.extract("brog"): any);
expect(result).toEqual({
id: "brog",
only: "ugg",
primitives: "ook",
});
});
it("handles objects that only have link fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Feline", id: "alpha"});
mirror.registerObject({typename: "Feline", id: "beta"});
mirror.registerObject({typename: "Feline", id: "gamma"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{
__typename: "Feline",
id: "alpha",
only: null,
lynx: {__typename: "Feline", id: "beta"},
},
{
__typename: "Feline",
id: "beta",
only: null,
lynx: {__typename: "Feline", id: "gamma"},
},
{
__typename: "Feline",
id: "gamma",
only: null,
lynx: null,
},
]);
const result = mirror.extract("alpha");
expect(result).toEqual({
id: "alpha",
only: null,
lynx: {
id: "beta",
only: null,
lynx: {
id: "gamma",
only: null,
lynx: null,
},
},
});
});
it("handles objects that only have connection fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Socket", id: "localhost:8080"});
mirror.registerObject({typename: "Socket", id: "localhost:7070"});
mirror.registerObject({typename: "Socket", id: "localhost:6060"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Socket", id: "localhost:8080"},
{__typename: "Socket", id: "localhost:7070"},
{__typename: "Socket", id: "localhost:6060"},
]);
const updateConnection = (
objectId: Schema.ObjectId,
fieldname: Schema.Fieldname,
ids: $ReadOnlyArray<Schema.ObjectId>
) => {
mirror._updateConnection(updateId, objectId, fieldname, {
totalCount: ids.length,
pageInfo: {hasNextPage: false, endCursor: String(ids.length)},
nodes: ids.map((id) => ({__typename: "Socket", id})),
});
};
updateConnection("localhost:8080", "only", []);
updateConnection("localhost:7070", "only", []);
updateConnection("localhost:6060", "only", []);
updateConnection("localhost:8080", "connections", ["localhost:7070"]);
updateConnection("localhost:7070", "connections", [
"localhost:6060",
"localhost:6060",
]);
updateConnection("localhost:6060", "connections", []);
const result = mirror.extract("localhost:8080");
expect(result).toEqual({
id: "localhost:8080",
only: [],
connections: [
{
id: "localhost:7070",
only: [],
connections: [
{id: "localhost:6060", only: [], connections: []},
{id: "localhost:6060", only: [], connections: []},
],
},
],
});
});
it("handles objects with no fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Empty", id: "mt"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [{__typename: "Empty", id: "mt"}]);
const result = mirror.extract("mt");
expect(result).toEqual({id: "mt"});
});
it("handles boolean primitives", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Caveman", id: "brog"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Caveman", id: "brog", only: false, primitives: true},
]);
expect(mirror.extract("brog")).toEqual({
id: "brog",
only: false,
primitives: true,
});
});
it("handles null primitives", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Caveman", id: "brog"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Caveman", id: "brog", only: null, primitives: null},
]);
expect(mirror.extract("brog")).toEqual({
id: "brog",
only: null,
primitives: null,
});
});
it("handles numeric primitives", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Caveman", id: "brog"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Caveman", id: "brog", only: 123, primitives: 987},
]);
expect(mirror.extract("brog")).toEqual({
id: "brog",
only: 123,
primitives: 987,
});
});
it("handles cyclic link structures", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Feline", id: "alpha"});
mirror.registerObject({typename: "Feline", id: "beta"});
mirror.registerObject({typename: "Feline", id: "gamma"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{
__typename: "Feline",
id: "alpha",
only: null,
lynx: {__typename: "Feline", id: "beta"},
},
{
__typename: "Feline",
id: "beta",
only: {__typename: "Feline", id: "beta"},
lynx: {__typename: "Feline", id: "gamma"},
},
{
__typename: "Feline",
id: "gamma",
only: {__typename: "Feline", id: "beta"},
lynx: null,
},
]);
const result: Feline = (mirror.extract("alpha"): any);
expect(result).toEqual({
id: "alpha",
only: null,
lynx: {
id: "beta",
only: result.lynx,
lynx: {
id: "gamma",
only: result.lynx,
lynx: null,
},
},
});
expect((result: any).lynx.only).toBe(result.lynx);
expect((result: any).lynx.lynx.only).toBe(result.lynx);
});
it("handles cyclic connection structures", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Socket", id: "localhost:8080"});
mirror.registerObject({typename: "Socket", id: "localhost:7070"});
mirror.registerObject({typename: "Socket", id: "localhost:6060"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Socket", id: "localhost:8080"},
{__typename: "Socket", id: "localhost:7070"},
{__typename: "Socket", id: "localhost:6060"},
]);
const updateConnection = (
objectId: Schema.ObjectId,
fieldname: Schema.Fieldname,
ids: $ReadOnlyArray<Schema.ObjectId>
) => {
mirror._updateConnection(updateId, objectId, fieldname, {
totalCount: ids.length,
pageInfo: {hasNextPage: false, endCursor: String(ids.length)},
nodes: ids.map((id) => ({__typename: "Socket", id})),
});
};
updateConnection("localhost:8080", "only", []);
updateConnection("localhost:7070", "only", []);
updateConnection("localhost:6060", "only", []);
updateConnection("localhost:8080", "connections", ["localhost:7070"]);
updateConnection("localhost:7070", "connections", [
"localhost:8080",
"localhost:7070",
"localhost:6060",
]);
updateConnection("localhost:6060", "connections", ["localhost:7070"]);
const result: Socket = (mirror.extract("localhost:8080"): any);
expect(result).toEqual({
id: "localhost:8080",
only: [],
connections: [
{
id: "localhost:7070",
only: [],
connections: [
result,
result.connections[0],
{
id: "localhost:6060",
only: [],
connections: [result.connections[0]],
},
],
},
],
});
const s8080: Socket = result;
const s7070: Socket = ((s8080.connections[0]: Socket | null): any);
const s6060: Socket = ((s7070.connections[2]: Socket | null): any);
expect(s7070.connections[0]).toBe(s8080);
expect(s7070.connections[1]).toBe(s7070);
expect(s7070.connections[2]).toBe(s6060);
expect(s6060.connections[0]).toBe(s7070);
});
it("handles connections with null and repeated values", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Socket", id: "localhost"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{__typename: "Socket", id: "localhost"},
]);
mirror._updateConnection(updateId, "localhost", "only", {
totalCount: 0,
pageInfo: {hasNextPage: false, endCursor: null},
nodes: [],
});
mirror._updateConnection(updateId, "localhost", "connections", {
totalCount: 6,
pageInfo: {hasNextPage: false, endCursor: "#6"},
nodes: [
null,
{__typename: "Socket", id: "localhost"},
null,
{__typename: "Socket", id: "localhost"},
{__typename: "Socket", id: "localhost"},
null,
],
});
const result: Socket = (mirror.extract("localhost"): any);
expect(result).toEqual({
id: "localhost",
only: [],
connections: [null, result, null, result, result, null],
});
expect(result.connections[1]).toBe(result);
expect(result.connections[3]).toBe(result);
expect(result.connections[4]).toBe(result);
});
it("handles a representative normal case", () => {
// In this test case, we have:
//
// - objects that are not relevant
// - object types with no relevant instances
// - object types with no instances at all
// - relevant objects that are not direct dependencies
// - relevant objects with cyclic links and connections
// - relevant objects with only primitive fields
// - relevant objects with empty connections
// - relevant objects with links pointing to `null`
// - relevant objects with links of union type
//
// (An object is "relevant" if it is a transitive dependency of
// the root.)
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const objects = {
repo: () => ({typename: "Repository", id: "repo:foo/bar"}),
issue1: () => ({typename: "Issue", id: "issue:#1"}),
issue2: () => ({typename: "Issue", id: "issue:#2"}),
issue3: () => ({typename: "Issue", id: "issue:#3"}),
alice: () => ({typename: "User", id: "user:alice"}),
bob: () => ({typename: "User", id: "user:bob"}),
ethereal: () => ({typename: "User", id: "user:ethereal"}),
nobody: () => ({typename: "User", id: "user:nobody"}),
noboty: () => ({typename: "Bot", id: "bot:noboty"}),
comment1: () => ({typename: "IssueComment", id: "comment:#2.1"}),
comment2: () => ({typename: "IssueComment", id: "comment:#2.2"}),
closedEvent: () => ({
typename: "ClosedEvent",
id: "issue:#2!closed#0",
}),
};
const asNode = ({typename, id}) => ({__typename: typename, id});
const update1 = mirror._createUpdate(new Date(123));
const update2 = mirror._createUpdate(new Date(234));
const update3 = mirror._createUpdate(new Date(345));
const emptyConnection = () => ({
totalCount: 0,
pageInfo: {
endCursor: null,
hasNextPage: false,
},
nodes: [],
});
// Update #1: Own data for the repository and issues #1 and #2
// and their authors. Connection data for issue #1 as a child of
// the repository, but not issue #2. No comments on any issue.
mirror.registerObject(objects.repo());
mirror.registerObject(objects.issue1());
mirror.registerObject(objects.issue2());
mirror.registerObject(objects.alice());
mirror.registerObject(objects.ethereal());
mirror._updateOwnData(update1, [
{
...asNode(objects.repo()),
url: "url://foo/bar",
},
]);
mirror._updateOwnData(update1, [
{
...asNode(objects.issue1()),
url: "url://issue/1",
author: asNode(objects.alice()),
repository: asNode(objects.repo()),
title: "this project looks dead; let's make some issues",
},
{
...asNode(objects.issue2()),
url: "url://issue/2",
author: asNode(objects.ethereal()),
repository: asNode(objects.repo()),
title: "by the time you read this, I will have deleted my account",
},
// issue:#3 remains unloaded
]);
mirror._updateOwnData(update1, [
{
...asNode(objects.alice()),
url: "url://alice",
login: "alice",
},
{
...asNode(objects.ethereal()),
login: "ethereal",
url: "url://ethereal",
},
// "nobody" and "noboty" remain unloaded
]);
mirror._updateConnection(update1, objects.repo().id, "issues", {
totalCount: 2,
pageInfo: {
endCursor: "cursor:repo:foo/bar.issues@update1",
hasNextPage: true,
},
nodes: [asNode(objects.issue1())],
});
mirror._updateConnection(
update1,
objects.issue1().id,
"comments",
emptyConnection()
);
mirror._updateConnection(
update1,
objects.issue1().id,
"timeline",
emptyConnection()
);
mirror._updateConnection(
update1,
objects.issue2().id,
"comments",
emptyConnection()
);
mirror._updateConnection(
update1,
objects.issue2().id,
"timeline",
emptyConnection()
);
// Update #2: Issue #2 author changes to `null`. Alice posts a
// comment on issue #2 and closes it. Issue #2 is loaded as a
// child of the repository.
mirror.registerObject(objects.comment1());
mirror._updateOwnData(update2, [
{
...asNode(objects.issue2()),
url: "url://issue/2",
author: null,
repository: asNode(objects.repo()),
title: "by the time you read this, I will have deleted my account",
},
// issue:#3 remains unloaded
]);
mirror._updateOwnData(update2, [
{
...asNode(objects.comment1()),
body: "cya",
author: asNode(objects.alice()),
},
]);
mirror._updateConnection(update2, objects.repo().id, "issues", {
totalCount: 2,
pageInfo: {
endCursor: "cursor:repo:foo/bar.issues@update2",
hasNextPage: true,
},
nodes: [asNode(objects.issue2())],
});
mirror._updateConnection(update2, objects.issue2().id, "comments", {
totalCount: 1,
pageInfo: {
endCursor: "cursor:issue:#2.comments@update2",
hasNextPage: false,
},
nodes: [asNode(objects.comment1())],
});
mirror._updateConnection(update2, objects.issue2().id, "timeline", {
totalCount: 1,
pageInfo: {
endCursor: "cursor:issue:#2.timeline@update2",
hasNextPage: false,
},
nodes: [asNode(objects.closedEvent())],
});
// Update #3: Bob comments on issue #2. An issue #3 is created
// but not yet added to the repository connection. The details
// for the closed event are fetched.
mirror.registerObject(objects.bob());
mirror.registerObject(objects.comment2());
mirror.registerObject(objects.issue3());
mirror._updateOwnData(update3, [
{
...asNode(objects.bob()),
url: "url://bob",
login: "bob",
},
]);
mirror._updateOwnData(update3, [
{
...asNode(objects.comment2()),
body: "alas, I did not know them well",
author: asNode(objects.bob()),
},
]);
mirror._updateOwnData(update3, [
{
...asNode(objects.issue3()),
url: "url://issue/3",
author: asNode(objects.bob()),
repository: asNode(objects.repo()),
title: "duly responding to the call for spurious issues",
},
]);
mirror._updateOwnData(update3, [
{
...asNode(objects.closedEvent()),
actor: asNode(objects.alice()),
},
]);
mirror._updateConnection(update3, objects.issue2().id, "comments", {
totalCount: 2,
pageInfo: {
endCursor: "cursor:issue:#2.comments@update3",
hasNextPage: false,
},
nodes: [asNode(objects.comment2())],
});
// The following entities are never referenced...
mirror.registerObject(objects.nobody());
mirror.registerObject(objects.noboty());
mirror.registerObject(objects.issue3());
const result = mirror.extract("repo:foo/bar");
expect(result).toEqual({
id: "repo:foo/bar",
url: "url://foo/bar",
issues: [
{
id: "issue:#1",
url: "url://issue/1",
author: {
id: "user:alice",
url: "url://alice",
login: "alice",
},
repository: result, // circular
title: "this project looks dead; let's make some issues",
comments: [],
timeline: [],
},
{
id: "issue:#2",
url: "url://issue/2",
author: null,
repository: result, // circular
title:
"by the time you read this, I will have deleted my account",
comments: [
{
id: "comment:#2.1",
body: "cya",
author: {
id: "user:alice",
url: "url://alice",
login: "alice",
},
},
{
id: "comment:#2.2",
body: "alas, I did not know them well",
author: {
id: "user:bob",
url: "url://bob",
login: "bob",
},
},
],
timeline: [
{
id: "issue:#2!closed#0",
actor: {
id: "user:alice",
url: "url://alice",
login: "alice",
},
},
],
},
],
});
});
});
}); });
describe("_buildSchemaInfo", () => { describe("_buildSchemaInfo", () => {