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:
parent
49f0803a7a
commit
1155c439b9
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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([
|
||||
|
|
Loading…
Reference in New Issue