mirror: add support for shallow-nested fields (#918)

Summary:
This commit follows up on the previous two pull requests by drawing the
rest of the owl.

Resolves #915.

Test Plan:
Unit tests included.

To verify the snapshot change, open the snapshot file, and copy
everything from `query TestUpdate {` through the matching `}`, not
including the enclosing quotes. Strip all backslashes (Jest adds them).
Post the resulting query to GitHub and verify that it completes
successfully and that the result contains a commit with an `author`.

In other words, `xsel -ob | tr -d '\\' | ghquery | jq .` with [ghquery].

[ghquery]: https://gist.github.com/wchargin/630e03e66fa084b7b2297189615326d1

The demo entry point has also been updated. For an end-to-end test, you
can run the following command to see a commit with a `null` author (with
the current state of the repository) and a commit with a non-`null`
author:

```
$ node bin/mirror.js /tmp/mirror-example.db \
>     Repository MDEwOlJlcG9zaXRvcnkxMjMyNTUwMDY= \
>     3600 2>/dev/null |
> jq '(.defaultBranchRef.target, .pullRequests[0].mergeCommit) | {url, author}'
{
  "url": "6bd1b4c0b7",
  "author": {
    "date": "2018-09-12T19:48:21-07:00",
    "user": null
  }
}
{
  "url": "0a223346b4",
  "author": {
    "date": "2018-02-28T00:43:47-08:00",
    "user": {
      "id": "MDQ6VXNlcjE0MDAwMjM=",
      "__typename": "User",
      "url": "https://github.com/decentralion",
      "login": "decentralion"
    }
  }
}
```

You can also check that it is possible to fetch the whole SourceCred
repository (ID: `MDEwOlJlcG9zaXRvcnkxMjAxNDU1NzA=`).

wchargin-branch: mirror-shallow
This commit is contained in:
William Chargin 2018-10-04 16:00:07 -07:00 committed by GitHub
parent 49f0803a7a
commit 1155c439b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 695 additions and 54 deletions

View File

@ -234,8 +234,8 @@ exports[`graphql/mirror Mirror _queryConnection snapshot test for actual GitHub
`;
exports[`graphql/mirror Mirror _updateOwnData snapshot test for actual GitHub queries 1`] = `
"query TestQuery {
node(id: \\"MDU6SXNzdWUzNDg1NDA0NjE=\\") {
"query TestUpdate {
issue: node(id: \\"MDU6SXNzdWUzNDg1NDA0NjE=\\") {
... on Issue {
__typename
id
@ -259,5 +259,19 @@ exports[`graphql/mirror Mirror _updateOwnData snapshot test for actual GitHub qu
title
}
}
commit: node(id: \\"MDY6Q29tbWl0MTIwMTQ1NTcwOjU1OTUwZjUzNTQ1NTEwOWJhNDhhYmYyYjk3N2U2NmFhMWNjMzVlNjk=\\") {
... on Commit {
__typename
id
oid
author {
date
user {
__typename
id
}
}
}
}
}"
`;

View File

@ -158,8 +158,10 @@ function schema(): Schema.Schema {
url: s.primitive(),
oid: s.primitive(),
message: s.primitive(),
// author omitted for now: GitActor has no `id`; see:
// https://github.com/sourcecred/sourcecred/issues/622#issuecomment-425220132
author: /* GitActor */ s.nested({
date: s.primitive(),
user: s.node("User"),
}),
}),
Tag: s.object({
id: s.id(),

View File

@ -112,6 +112,21 @@ export class Mirror {
* a type; querying connection data, by contrast, requires the
* object-specific end cursor.
*
* Nested fields merit additional explanation. The nested field itself
* exists on the primitives table with SQL value either NULL, 0, or 1
* (as SQL integers, not strings). As with all other primitives,
* `NULL` indicates that the value has never been fetched. If the
* value has been fetched, it is 0 if the nested field itself was
* `null` on the GraphQL result, or 1 if it was present. This field
* lets us distinguish "author: null" from "author: {user: null}".
*
* The "eggs" of a nested field are treated as normal primitive or
* link values, whose fieldname is the nested fieldname and egg
* fieldname joined by a period. So, if object type `Foo` has nested
* field `bar: Schema.nested({baz: Schema.primitive()})`, then the
* `primitives_Foo` table will include a column "bar.baz". Likewise, a
* row in the `links` table might have fieldname 'quux.zod'.
*
* All aforementioned tables are keyed by object ID. Each object also
* appears once in the `objects` table, which relates its ID,
* typename, and last own-data update. Each connection has its own
@ -138,7 +153,7 @@ export class Mirror {
// interpreted. If you've made a change and you're not sure whether
// it requires bumping the version, bump it: requiring some extra
// one-time cache resets is okay; doing the wrong thing is not.
const blob = stringify({version: "MIRROR_v1", schema: this._schema});
const blob = stringify({version: "MIRROR_v2", schema: this._schema});
const db = this._db;
_inTransaction(db, () => {
// We store the metadata in a singleton table `meta`, whose unique row
@ -269,12 +284,36 @@ export class Mirror {
throw new Error("invalid field name: " + JSON.stringify(fieldname));
}
}
for (const fieldname of type.nestedFieldNames) {
if (!isSqlSafe(fieldname)) {
throw new Error("invalid field name: " + JSON.stringify(fieldname));
}
const children = type.nestedFields[fieldname].primitives;
for (const childFieldname of Object.keys(children)) {
if (!isSqlSafe(childFieldname)) {
throw new Error(
"invalid field name: " +
JSON.stringify(childFieldname) +
" under " +
JSON.stringify(fieldname)
);
}
}
}
const tableName = _primitivesTableName(typename);
const tableSpec = [
"id TEXT NOT NULL PRIMARY KEY",
...type.primitiveFieldNames.map((fieldname) => `"${fieldname}"`),
"FOREIGN KEY(id) REFERENCES objects(id)",
].join(", ");
const tableSpec = []
.concat(
["id TEXT NOT NULL PRIMARY KEY"],
type.primitiveFieldNames.map((fieldname) => `"${fieldname}"`),
type.nestedFieldNames.map((fieldname) => `"${fieldname}"`),
...type.nestedFieldNames.map((f1) =>
Object.keys(type.nestedFields[f1].primitives).map(
(f2) => `"${f1}.${f2}"`
)
),
["FOREIGN KEY(id) REFERENCES objects(id)"]
)
.join(", ");
db.prepare(`CREATE TABLE ${tableName} (${tableSpec})`).run();
}
});
@ -381,6 +420,13 @@ export class Mirror {
for (const fieldname of objectType.linkFieldNames) {
addLink.run({id, fieldname});
}
for (const parentFieldname of objectType.nestedFieldNames) {
const children = objectType.nestedFields[parentFieldname].nodes;
for (const childFieldname of Object.keys(children)) {
const fieldname = `${parentFieldname}.${childFieldname}`;
addLink.run({id, fieldname});
}
}
for (const fieldname of objectType.connectionFieldNames) {
addConnection.run({id, fieldname});
}
@ -1057,7 +1103,26 @@ export class Mirror {
// Not handled by this function.
return null;
case "NESTED":
throw new Error("Nested fields not supported.");
return b.field(
fieldname,
{},
Object.keys(field.eggs).map((childFieldname) => {
const childField = field.eggs[childFieldname];
switch (childField.type) {
case "PRIMITIVE":
return b.field(childFieldname);
case "NODE":
return b.field(
childFieldname,
{},
this._queryShallow(childField.elementType)
);
// istanbul ignore next
default:
throw new Error((childField.type: empty));
}
})
);
// istanbul ignore next
default:
throw new Error((field.type: empty));
@ -1148,30 +1213,69 @@ export class Mirror {
}
// Update each node's primitive data.
//
// (This typedef would be in the following block statement but for
// facebook/flow#6961.)
declare opaque type ParameterName: string;
{
const parameterNameFor = {
topLevelField(fieldname: Schema.Fieldname): ParameterName {
return ((["t", fieldname].join("_"): string): any);
},
nestedField(
nestFieldname: Schema.Fieldname,
eggFieldname: Schema.Fieldname
): ParameterName {
return (([
"n",
String(nestFieldname.length),
nestFieldname,
eggFieldname,
].join("_"): string): any);
},
};
const updatePrimitives: ({|
+id: Schema.ObjectId,
+[primitiveFieldName: Schema.Fieldname]: string,
// These keys can be top-level primitive fields or the primitive
// children of a nested field. The values are the JSON encodings
// of the values received from the GraphQL response. For a
// nested field, the value is `0` or `1` as the nested field is
// `null` or not. (See docs on `_initialize` for more details.)
+[parameterName: ParameterName]: string | 0 | 1,
|}) => void = (() => {
if (objectType.primitiveFieldNames.length === 0) {
const updates: $ReadOnlyArray<string> = [].concat(
objectType.primitiveFieldNames.map(
(f) => `"${f}" = :${parameterNameFor.topLevelField(f)}`
),
objectType.nestedFieldNames.map(
(f) => `"${f}" = :${parameterNameFor.topLevelField(f)}`
),
...objectType.nestedFieldNames.map((f1) =>
Object.keys(objectType.nestedFields[f1].primitives).map(
(f2) => `"${f1}.${f2}" = :${parameterNameFor.nestedField(f1, f2)}`
)
)
);
if (updates.length === 0) {
return () => {};
}
const tableName = _primitivesTableName(typename);
const updates = objectType.primitiveFieldNames
.map((f) => `"${f}" = :${f}`)
.join(", ");
const stmt = db.prepare(
`UPDATE ${tableName} SET ${updates} WHERE id = :id`
`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = :id`
);
return _makeSingleUpdateFunction(stmt);
})();
for (const entry of queryResult) {
const primitives: {|
+id: Schema.ObjectId,
[primitiveFieldName: Schema.Fieldname]: string,
[ParameterName]: string | 0 | 1,
|} = {id: entry.id};
// Add top-level primitives.
for (const fieldname of objectType.primitiveFieldNames) {
const value: PrimitiveResult | NodeFieldResult = entry[fieldname];
const value: PrimitiveResult | NodeFieldResult | NestedFieldResult =
entry[fieldname];
const primitive: PrimitiveResult = (value: any);
if (primitive === undefined) {
const s = JSON.stringify;
@ -1180,8 +1284,49 @@ export class Mirror {
`of type ${s(typename)} (got ${(primitive: empty)})`
);
}
primitives[fieldname] = JSON.stringify(primitive);
primitives[
parameterNameFor.topLevelField(fieldname)
] = JSON.stringify(primitive);
}
// Add nested primitives.
for (const nestFieldname of objectType.nestedFieldNames) {
const nestValue:
| PrimitiveResult
| NodeFieldResult
| NestedFieldResult =
entry[nestFieldname];
const topLevelNested: NestedFieldResult = (nestValue: any);
if (topLevelNested === undefined) {
const s = JSON.stringify;
throw new Error(
`Missing nested field ` +
`${s(nestFieldname)} on ${s(entry.id)} ` +
`of type ${s(typename)} (got ${(topLevelNested: empty)})`
);
}
primitives[parameterNameFor.topLevelField(nestFieldname)] =
topLevelNested == null ? 0 : 1;
const eggFields = objectType.nestedFields[nestFieldname].primitives;
for (const eggFieldname of Object.keys(eggFields)) {
const eggValue: PrimitiveResult | NodeFieldResult =
topLevelNested == null ? null : topLevelNested[eggFieldname];
const primitive: PrimitiveResult = (eggValue: any);
if (primitive === undefined) {
const s = JSON.stringify;
throw new Error(
`Missing nested field ` +
`${s(nestFieldname)}.${s(eggFieldname)} ` +
`on ${s(entry.id)} ` +
`of type ${s(typename)} (got ${(primitive: empty)})`
);
}
primitives[
parameterNameFor.nestedField(nestFieldname, eggFieldname)
] = JSON.stringify(primitive);
}
}
updatePrimitives(primitives);
}
}
@ -1201,9 +1346,12 @@ export class Mirror {
);
return _makeSingleUpdateFunction(stmt);
})();
for (const entry of queryResult) {
// Add top-level links.
for (const fieldname of objectType.linkFieldNames) {
const value: PrimitiveResult | NodeFieldResult = entry[fieldname];
const value: PrimitiveResult | NodeFieldResult | NestedFieldResult =
entry[fieldname];
const link: NodeFieldResult = (value: any);
if (link === undefined) {
const s = JSON.stringify;
@ -1216,6 +1364,39 @@ export class Mirror {
const parentId = entry.id;
updateLink({parentId, fieldname, childId});
}
// Add nested links.
for (const nestFieldname of objectType.nestedFieldNames) {
const nestValue:
| PrimitiveResult
| NodeFieldResult
| NestedFieldResult =
entry[nestFieldname];
const topLevelNested: NestedFieldResult = (nestValue: any);
// No need for an extra safety check that this is present: we
// handled that while covering primitive fields.
const childFields = objectType.nestedFields[nestFieldname].nodes;
for (const childFieldname of Object.keys(childFields)) {
const childValue: PrimitiveResult | NodeFieldResult =
topLevelNested == null ? null : topLevelNested[childFieldname];
const link: NodeFieldResult = (childValue: any);
if (link === undefined) {
const s = JSON.stringify;
throw new Error(
`Missing nested field ` +
`${s(nestFieldname)}.${s(childFieldname)} ` +
`on ${s(entry.id)} ` +
`of type ${s(typename)} (got ${(link: empty)})`
);
}
const childId = this._nontransactionallyRegisterNodeFieldResult(
link
);
const fieldname = `${nestFieldname}.${childFieldname}`;
const parentId = entry.id;
updateLink({parentId, fieldname, childId});
}
}
}
}
@ -1229,12 +1410,16 @@ export class Mirror {
* The result is an object whose keys are fieldnames, and whose values
* are:
*
* - for the ID field: the object ID;
* - for "__typename": the object's GraphQL typename;
* - for "id": the object's GraphQL 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`.
* extracted objects, each of which may be `null`;
* - for nested fields: an object with primitive and node reference
* fields in the same form as above (note: nested objects do not
* automatically have a "__typename" field).
*
* For instance, the result of `extract("issue:1")` might be:
*
@ -1268,8 +1453,26 @@ export class Mirror {
* },
* },
* ],
* timeline: [
* {
* __typename: "Commit",
* messageHeadline: "reinstate CPU warmer (fixes #1172)",
* author: {
* date: "2001-02-03T04:05:06-07:00",
* user: {
* __typename: "User",
* id: "user:admin",
* login: "admin",
* }
* }
* }
* ],
* }
*
* (Here, "title" is a primitive, the "author" field on an issue is a
* node reference, "comments" is a connection, and the "author" field
* on a commit is a nested object.)
*
* The returned structure may be circular.
*
* If a node appears more than one time in the result---for instance,
@ -1384,20 +1587,29 @@ export class Mirror {
);
}
const primitivesTableName = _primitivesTableName(typename);
const selections = [
`${primitivesTableName}.id AS id`,
...objectType.primitiveFieldNames.map(
const selections: $ReadOnlyArray<string> = [].concat(
[`${primitivesTableName}.id AS id`],
objectType.primitiveFieldNames.map(
(fieldname) =>
`${primitivesTableName}."${fieldname}" AS "${fieldname}"`
),
].join(", ");
objectType.nestedFieldNames.map(
(fieldname) =>
`${primitivesTableName}."${fieldname}" AS "${fieldname}"`
),
...objectType.nestedFieldNames.map((f1) =>
Object.keys(objectType.nestedFields[f1].primitives).map(
(f2) => `${primitivesTableName}."${f1}.${f2}" AS "${f1}.${f2}"`
)
)
);
const rows: $ReadOnlyArray<{|
+id: Schema.ObjectId,
+[Schema.Fieldname]: string,
+[Schema.Fieldname]: string | 0 | 1,
|}> = db
.prepare(
dedent`\
SELECT ${selections}
SELECT ${selections.join(", ")}
FROM ${temporaryTableName} JOIN ${primitivesTableName}
USING (id)
`
@ -1407,9 +1619,50 @@ export class Mirror {
const object = {};
object.id = row.id;
object.__typename = typename;
for (const fieldname of objectType.nestedFieldNames) {
const isPresent: string | 0 | 1 = row[fieldname];
// istanbul ignore if: should not be reachable
if (isPresent !== 0 && isPresent !== 1) {
const s = JSON.stringify;
const id = object.id;
throw new Error(
`Corruption: nested field ${s(fieldname)} on ${s(id)} ` +
`set to ${String(isPresent)}`
);
}
if (isPresent) {
// We'll add primitives and links onto this object.
object[fieldname] = {};
} else {
object[fieldname] = null;
}
}
for (const key of Object.keys(row)) {
if (key === "id") continue;
object[key] = JSON.parse(row[key]);
const rawValue = row[key];
if (rawValue === 0 || rawValue === 1) {
// Name of a nested field; already processed.
continue;
}
const value = JSON.parse(rawValue);
const parts = key.split(".");
switch (parts.length) {
case 1:
object[key] = value;
break;
case 2: {
const [nestName, eggName] = parts;
if (object[nestName] !== null) {
object[nestName][eggName] = value;
}
break;
}
// istanbul ignore next: should not be possible
default:
throw new Error(
`Corruption: bad field name: ${JSON.stringify(key)}`
);
}
}
allObjects.set(object.id, object);
}
@ -1437,7 +1690,25 @@ export class Mirror {
link.childId == null
? null
: NullUtil.get(allObjects.get(link.childId));
parent[link.fieldname] = child;
const {fieldname} = link;
const parts = fieldname.split(".");
switch (parts.length) {
case 1:
parent[fieldname] = child;
break;
case 2: {
const [nestName, eggName] = parts;
if (parent[nestName] !== null) {
parent[nestName][eggName] = child;
}
break;
}
// istanbul ignore next: should not be possible
default:
throw new Error(
`Corruption: bad link name: ${JSON.stringify(fieldname)}`
);
}
}
}
@ -1676,6 +1947,9 @@ type ConnectionFieldResult = {|
+pageInfo: {|+hasNextPage: boolean, +endCursor: string | null|},
+nodes: $ReadOnlyArray<NodeFieldResult>,
|};
type NestedFieldResult = {
+[Schema.Fieldname]: PrimitiveResult | NodeFieldResult,
} | null;
/**
* Result describing own-data for many nodes of a given type. Whether a
@ -1689,7 +1963,8 @@ type OwnDataUpdateResult = $ReadOnlyArray<{
+id: Schema.ObjectId,
+[nonConnectionFieldname: Schema.Fieldname]:
| PrimitiveResult
| NodeFieldResult,
| NodeFieldResult
| NestedFieldResult,
}>;
/**

View File

@ -60,6 +60,14 @@ describe("graphql/mirror", () => {
body: s.primitive(),
author: s.node("Actor"),
}),
Commit: s.object({
id: s.id(),
oid: s.primitive(),
author: /* GitActor */ s.nested({
date: s.primitive(),
user: s.node("User"),
}),
}),
IssueTimelineItem: s.union(issueTimelineItemClauses()),
Actor: s.union(["User", "Bot", "Organization"]), // actually an interface
User: s.object({
@ -193,7 +201,7 @@ describe("graphql/mirror", () => {
);
});
it("rejects a schema with SQL-unsafe field name", () => {
it("rejects a schema with SQL-unsafe primitive field name", () => {
const s = Schema;
const schema0 = s.schema({
A: s.object({id: s.id(), "Non-Word-Characters": s.primitive()}),
@ -204,6 +212,31 @@ describe("graphql/mirror", () => {
);
});
it("rejects a schema with SQL-unsafe nested field name", () => {
const s = Schema;
const schema0 = s.schema({
A: s.object({id: s.id(), "Non-Word-Characters": s.nested({})}),
});
const db = new Database(":memory:");
expect(() => new Mirror(db, schema0)).toThrow(
'invalid field name: "Non-Word-Characters"'
);
});
it("rejects a schema with SQL-unsafe nested primitive field name", () => {
const s = Schema;
const schema0 = s.schema({
A: s.object({
id: s.id(),
problem: s.nested({"Non-Word-Characters": s.primitive()}),
}),
});
const db = new Database(":memory:");
expect(() => new Mirror(db, schema0)).toThrow(
'invalid field name: "Non-Word-Characters" under "problem"'
);
});
it("allows specifying a good schema after rejecting one", () => {
const s = Schema;
const schema0 = s.schema({
@ -1689,6 +1722,21 @@ describe("graphql/mirror", () => {
// no `timeline`
]);
});
it("includes nested primitives and nodes", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const query = mirror._queryOwnData("Commit");
const b = Queries.build;
expect(query).toEqual([
b.field("__typename"),
b.field("id"),
b.field("oid"),
b.field("author", {}, [
b.field("date"),
b.field("user", {}, [b.field("__typename"), b.field("id")]),
]),
]);
});
});
describe("_updateOwnData", () => {
@ -1817,6 +1865,69 @@ describe("graphql/mirror", () => {
"(got undefined)"
);
});
it("fails if the input is missing any nested fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const updateId = mirror._createUpdate(new Date(123));
mirror.registerObject({typename: "Commit", id: "commit:oid"});
expect(() => {
mirror._updateOwnData(updateId, [
{
__typename: "Commit",
id: "commit:oid",
oid: "yes",
// author omitted
},
]);
}).toThrow(
'Missing nested field "author" on "commit:oid" of type "Commit" ' +
"(got undefined)"
);
});
it("fails if the input is missing any nested primitive fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const updateId = mirror._createUpdate(new Date(123));
mirror.registerObject({typename: "Commit", id: "commit:oid"});
expect(() => {
mirror._updateOwnData(updateId, [
{
__typename: "Commit",
id: "commit:oid",
oid: "yes",
author: {
// date omitted
user: {__typename: "User", id: "user:alice"},
},
},
]);
}).toThrow(
'Missing nested field "author"."date" on "commit:oid" ' +
'of type "Commit" (got undefined)'
);
});
it("fails if the input is missing any nested link fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const updateId = mirror._createUpdate(new Date(123));
mirror.registerObject({typename: "Commit", id: "commit:oid"});
expect(() => {
mirror._updateOwnData(updateId, [
{
__typename: "Commit",
id: "commit:oid",
oid: "yes",
author: {
date: "today",
// user omitted
},
},
]);
}).toThrow(
'Missing nested field "author"."user" on "commit:oid" ' +
'of type "Commit" (got undefined)'
);
});
it("properly stores normal data", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
@ -1860,6 +1971,123 @@ describe("graphql/mirror", () => {
{id: "issue:#3", url: null, title: null},
]);
});
it("stores data with non-`null` nested fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const updateId = mirror._createUpdate(new Date(123));
mirror.registerObject({typename: "Commit", id: "commit:oid"});
mirror.registerObject({typename: "Commit", id: "commit:zzz"});
mirror._updateOwnData(updateId, [
{
__typename: "Commit",
id: "commit:oid",
oid: "yes",
author: {
date: "today",
user: {__typename: "User", id: "user:alice"},
},
},
{
__typename: "Commit",
id: "commit:zzz",
oid: "zzz",
author: {
date: null,
user: null,
},
},
]);
expect(
db.prepare("SELECT * FROM primitives_Commit ORDER BY id ASC").all()
).toEqual([
{
id: "commit:oid",
oid: '"yes"',
author: +true,
"author.date": '"today"',
},
{
id: "commit:zzz",
oid: '"zzz"',
author: +true,
"author.date": "null",
},
]);
expect(
db.prepare("SELECT * FROM links ORDER BY parent_id ASC").all()
).toEqual([
{
rowid: expect.anything(),
parent_id: "commit:oid",
fieldname: "author.user",
child_id: "user:alice",
},
{
rowid: expect.anything(),
parent_id: "commit:zzz",
fieldname: "author.user",
child_id: null,
},
]);
expect(
db
.prepare("SELECT COUNT(1) FROM objects WHERE id = 'user:alice'")
.pluck()
.get()
).toEqual(1);
});
it("stores data with `null` nested fields", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const update1 = mirror._createUpdate(new Date(123));
const update2 = mirror._createUpdate(new Date(456));
mirror.registerObject({typename: "Commit", id: "commit:oid"});
mirror._updateOwnData(update1, [
{
__typename: "Commit",
id: "commit:oid",
oid: "yes",
author: {
date: "today",
user: {__typename: "User", id: "user:alice"},
},
},
]);
mirror._updateOwnData(update2, [
{
__typename: "Commit",
id: "commit:oid",
oid: "mmm",
author: null,
},
]);
expect(
db.prepare("SELECT * FROM primitives_Commit ORDER BY id ASC").all()
).toEqual([
{
id: "commit:oid",
oid: '"mmm"',
author: +false,
"author.date": "null",
},
]);
expect(
db.prepare("SELECT * FROM links ORDER BY parent_id ASC").all()
).toEqual([
{
rowid: expect.anything(),
parent_id: "commit:oid",
fieldname: "author.user",
child_id: null,
},
]);
expect(
db
.prepare("SELECT COUNT(1) FROM objects WHERE id = 'user:alice'")
.pluck()
.get()
).toEqual(1);
});
it("properly handles input of a type with no primitives", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
@ -1942,14 +2170,25 @@ describe("graphql/mirror", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildGithubSchema());
const exampleIssueId = "MDU6SXNzdWUzNDg1NDA0NjE=";
const exampleCommitId =
"MDY6Q29tbWl0MTIwMTQ1NTcwOjU1OTUwZjUzNTQ1NTEwOWJhNDhhYmYyYjk3N2U2NmFhMWNjMzVlNjk=";
const b = Queries.build;
const query = b.query(
"TestQuery",
"TestUpdate",
[],
[
b.field("node", {id: b.literal(exampleIssueId)}, [
b.inlineFragment("Issue", mirror._queryOwnData("Issue")),
]),
b.alias(
"issue",
b.field("node", {id: b.literal(exampleIssueId)}, [
b.inlineFragment("Issue", mirror._queryOwnData("Issue")),
])
),
b.alias(
"commit",
b.field("node", {id: b.literal(exampleCommitId)}, [
b.inlineFragment("Commit", mirror._queryOwnData("Commit")),
])
),
]
);
const format = (body: Queries.Body): string =>
@ -1978,6 +2217,14 @@ describe("graphql/mirror", () => {
only: s.connection("Socket"),
connections: s.connection("Socket"),
}),
Nest: s.object({
id: s.id(),
nest: s.nested({
egg: s.primitive(),
cat: s.node("Feline"),
absent: s.node("Empty"),
}),
}),
Empty: s.object({
id: s.id(),
}),
@ -2001,6 +2248,19 @@ describe("graphql/mirror", () => {
+only: $ReadOnlyArray<null | Socket>,
+connections: $ReadOnlyArray<null | Socket>,
|};
type Nest = {|
+__typename: "Nest",
+id: string,
+nest: {|
+egg: mixed,
+cat: null | Feline,
+empty: null | Empty,
|},
|};
type Empty = {|
+__typename: "Empty",
+id: string,
|};
it("fails if the provided object does not exist", () => {
const db = new Database(":memory:");
@ -2305,6 +2565,48 @@ describe("graphql/mirror", () => {
});
});
it("handles nested objects", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
mirror.registerObject({typename: "Nest", id: "eyrie"});
mirror.registerObject({typename: "Feline", id: "meow"});
const updateId = mirror._createUpdate(new Date(123));
mirror._updateOwnData(updateId, [
{
__typename: "Nest",
id: "eyrie",
nest: {
egg: "nog",
cat: {__typename: "Feline", id: "meow"},
absent: null,
},
},
]);
mirror._updateOwnData(updateId, [
{
__typename: "Feline",
id: "meow",
only: {__typename: "Feline", id: "meow"},
lynx: null,
},
]);
const result: Nest = (mirror.extract("eyrie"): any);
expect(result).toEqual({
__typename: "Nest",
id: "eyrie",
nest: {
egg: "nog",
cat: {
__typename: "Feline",
id: "meow",
only: result.nest.cat,
lynx: null,
},
absent: null,
},
});
});
it("handles cyclic link structures", () => {
const db = new Database(":memory:");
const mirror = new Mirror(db, buildTestSchema());
@ -2467,6 +2769,8 @@ describe("graphql/mirror", () => {
// - relevant objects with empty connections
// - relevant objects with links pointing to `null`
// - relevant objects with links of union type
// - relevant objects with non-`null` nested fields
// - relevant objects with `null` nested fields
//
// (An object is "relevant" if it is a transitive dependency of
// the root.)
@ -2489,8 +2793,16 @@ describe("graphql/mirror", () => {
typename: "ClosedEvent",
id: "issue:#2!closed#0",
}),
commit1: () => ({typename: "Commit", id: "commit:oid"}),
commit2: () => ({typename: "Commit", id: "commit:zzz"}),
};
const asNode = ({typename, id}) => ({__typename: typename, id});
const asNode = ({
typename,
id,
}): {|+__typename: Schema.Typename, +id: Schema.ObjectId|} => ({
__typename: typename,
id,
});
const update1 = mirror._createUpdate(new Date(123));
const update2 = mirror._createUpdate(new Date(234));
@ -2584,7 +2896,8 @@ describe("graphql/mirror", () => {
// 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.
// child of the repository. Alice adds a commit to issue #1, and
// an anonymous user also adds a commit to issue #1.
mirror.registerObject(objects.comment1());
mirror._updateOwnData(update2, [
{
@ -2619,6 +2932,14 @@ describe("graphql/mirror", () => {
},
nodes: [asNode(objects.comment1())],
});
mirror._updateConnection(update2, objects.issue1().id, "timeline", {
totalCount: 2,
pageInfo: {
endCursor: "cursor:issue:#1.timeline@update2",
hasNextPage: false,
},
nodes: [asNode(objects.commit1()), asNode(objects.commit2())],
});
mirror._updateConnection(update2, objects.issue2().id, "timeline", {
totalCount: 1,
pageInfo: {
@ -2630,10 +2951,30 @@ describe("graphql/mirror", () => {
// 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.
// for the commits and the closed event are fetched.
mirror.registerObject(objects.bob());
mirror.registerObject(objects.comment2());
mirror.registerObject(objects.issue3());
mirror._updateOwnData(update3, [
{
...asNode(objects.commit1()),
oid: "yes",
author: {
date: "today",
user: {
__typename: "User",
id: "user:alice",
},
},
},
]);
mirror._updateOwnData(update3, [
{
...asNode(objects.commit2()),
oid: "hmm",
author: null,
},
]);
mirror._updateOwnData(update3, [
{
...asNode(objects.bob()),
@ -2696,7 +3037,28 @@ describe("graphql/mirror", () => {
repository: result, // circular
title: "this project looks dead; let's make some issues",
comments: [],
timeline: [],
timeline: [
{
__typename: "Commit",
id: "commit:oid",
oid: "yes",
author: {
date: "today",
user: {
__typename: "User",
id: "user:alice",
url: "url://alice",
login: "alice",
},
},
},
{
__typename: "Commit",
id: "commit:zzz",
oid: "hmm",
author: null,
},
],
},
{
__typename: "Issue",
@ -2751,19 +3113,7 @@ describe("graphql/mirror", () => {
describe("_buildSchemaInfo", () => {
it("processes object types properly", () => {
const s = Schema;
const schema = {
...buildGithubSchema(),
Commit: s.object({
id: s.id(),
oid: s.primitive(),
author: /* GitActor */ s.nested({
date: s.primitive(),
user: s.node("User"),
}),
}),
};
const result = _buildSchemaInfo(schema);
const result = _buildSchemaInfo(buildGithubSchema());
expect(Object.keys(result.objectTypes).sort()).toEqual(
Array.from(
new Set([