Skip to content

Commit

Permalink
TS: Add edges to Patterns (#506)
Browse files Browse the repository at this point in the history
  • Loading branch information
lolopinto authored Sep 21, 2021
1 parent 87e55c7 commit 1a704e4
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 148 deletions.
39 changes: 31 additions & 8 deletions internal/schema/input/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import (
)

type Schema struct {
Nodes map[string]*Node
Nodes map[string]*Node `json:"schemas"`
Patterns map[string]*Pattern `json:"patterns"`
}

type Pattern struct {
Name string `json:"name"`
AssocEdges []*AssocEdge `json:"assocEdges"`
}

type Node struct {
Expand All @@ -27,6 +33,8 @@ type Node struct {
Constraints []*Constraint `json:"constraints"`
Indices []*Index `json:"indices"`
HideFromGraphQL bool `json:"hideFromGraphQL"`
EdgeConstName string `json:"edgeConstName"`
PatternName string `json:"patternName"`
}

func (n *Node) AddAssocEdge(edge *AssocEdge) {
Expand Down Expand Up @@ -555,16 +563,31 @@ func (g *AssocEdgeGroup) AddAssocEdge(edge *AssocEdge) {
type InverseAssocEdge struct {
// TODO need to be able to mark this as unique
// this is an easy way to get 1->many
Name string `json:"name"`
Name string `json:"name"`
EdgeConstName string `json:"edgeConstName"`
}

func ParseSchema(input []byte) (*Schema, error) {
nodes := make(map[string]*Node)
if err := json.Unmarshal(input, &nodes); err != nil {
return nil, err
s := &Schema{}
if err := json.Unmarshal(input, s); err != nil {
// don't think this applies but keeping it here just in case
nodes := make(map[string]*Node)
if err := json.Unmarshal(input, &nodes); err != nil {
return nil, err
}
return &Schema{Nodes: nodes}, nil
}
// in the old route, it doesn't throw an error but just unmarshalls nothing 😭
// TestCustomFields
// also need to verify TestCustomListQuery|TestCustomUploadType works
// so checking s.Nodes == nil instead of len() == 0
if s.Nodes == nil {
nodes := make(map[string]*Node)
if err := json.Unmarshal(input, &nodes); err != nil {
return nil, err
}
return &Schema{Nodes: nodes}, nil
}

return &Schema{
Nodes: nodes,
}, nil
return s, nil
}
2 changes: 1 addition & 1 deletion ts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@snowtop/ent",
"version": "0.0.11",
"version": "0.0.13",
"description": "snowtop ent framework",
"main": "index.js",
"types": "index.d.ts",
Expand Down
44 changes: 42 additions & 2 deletions ts/src/core/query/assoc_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ export type EdgeQuerySource<T extends Ent> =
| ID[]
| EdgeQuery<T, AssocEdge>;

type loaderOptionsFunc = (type: string) => LoadEntOptions<Ent>;

interface typeData {
ids: ID[];
options: LoadEntOptions<Ent>;
}

export class AssocEdgeQueryBase<
TSource extends Ent,
TDest extends Ent,
TEdge extends AssocEdge
TEdge extends AssocEdge,
> extends BaseEdgeQuery<TDest, TEdge> {
private idsResolved: boolean;
private resolvedIDs: ID[] = [];
Expand All @@ -31,7 +38,9 @@ export class AssocEdgeQueryBase<
public src: EdgeQuerySource<TSource>,
private countLoaderFactory: AssocEdgeCountLoaderFactory,
private dataLoaderFactory: AssocEdgeLoaderFactory<TEdge>,
private options: LoadEntOptions<TDest>,
// if function, it's a polymorphic edge and need to provide
// a function that goes from edgeType to LoadEntOptions
private options: LoadEntOptions<TDest> | loaderOptionsFunc,
) {
super(viewer, "time");
}
Expand Down Expand Up @@ -107,6 +116,37 @@ export class AssocEdgeQueryBase<
id: ID,
edges: AssocEdge[],
): Promise<TDest[]> {
if (typeof this.options === "function") {
const m = new Map<string, typeData>();
for (const edge of edges) {
const nodeType = edge.id2Type;
let data = m.get(nodeType);
if (data === undefined) {
const opts = this.options(nodeType);
if (opts === undefined) {
throw new Error(
`getLoaderOptions returned undefined for type ${nodeType}`,
);
}
data = {
ids: [],
options: opts,
};
}
data.ids.push(edge.id2);
m.set(nodeType, data);
}
let promises: Promise<Ent[]>[] = [];
for (const [_, value] of m) {
promises.push(loadEnts(this.viewer, value.options, ...value.ids));
}
const entss = await Promise.all(promises);
const r: Ent[] = [];
for (const ents of entss) {
r.push(...ents);
}
return r as TDest[];
}
const ids = edges.map((edge) => edge.id2);
return await loadEnts(this.viewer, this.options, ...ids);
}
Expand Down
193 changes: 193 additions & 0 deletions ts/src/core/query/shared_assoc_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { advanceBy } from "jest-date-mock";
import {
FakeUser,
UserToContactsQuery,
UserToFollowingQuery,
FakeContact,
EdgeType,
getUserBuilder,
Expand All @@ -18,6 +19,7 @@ import {
NodeType,
UserToCustomEdgeQuery,
CustomEdge,
getEventBuilder,
} from "../../testutils/fake_data/index";
import {
inputs,
Expand All @@ -27,6 +29,7 @@ import {
verifyUserToContactEdges,
verifyUserToContacts,
createTestEvent,
getEventInput,
} from "../../testutils/fake_data/test_helpers";
import DB, { Dialect } from "../db";

Expand Down Expand Up @@ -604,4 +607,194 @@ export function assocTests() {
await filter.testEnts();
});
});

class PolymorphicID2sTestQueryFilter {
user: FakeUser;
users: FakeUser[] = [];
events: FakeEvent[] = [];
expCount: number;
constructor(
private filter: (q: UserToFollowingQuery) => UserToFollowingQuery,
private ents: (ent: Ent[]) => Ent[],
private limit?: number,
) {}

async beforeEach() {
this.users = [];
this.events = [];
this.user = await createTestUser();
for (let i = 0; i < 5; i++) {
advanceBy(100);

const builder = getUserBuilder(this.user.viewer, getUserInput());
builder.orchestrator.addOutboundEdge(
this.user.id,
EdgeType.ObjectToFollowedUsers,
NodeType.FakeUser,
);
await builder.saveX();
const user2 = await builder.editedEntX();
this.users.push(user2);
}
for (let i = 0; i < 5; i++) {
advanceBy(100);

const builder = getEventBuilder(
this.user.viewer,
getEventInput(this.user),
);
builder.orchestrator.addOutboundEdge(
this.user.id,
EdgeType.ObjectToFollowedUsers,
NodeType.FakeUser,
);
await builder.saveX();
const event = await builder.editedEntX();
this.events.push(event);
}
//order is users, then events
this.expCount = this.ents([...this.users, ...this.events]).length;

QueryRecorder.clearQueries();
}

getQuery(viewer?: Viewer) {
return this.filter(
UserToFollowingQuery.query(
viewer || new IDViewer(this.user.id),
this.user,
),
);
}

async testIDs() {
const ids = await this.getQuery().queryIDs();

expect(ids.length).toBe(this.expCount);

const expIDs = this.users
.map((user) => user.id)
.concat(this.events.map((event) => event.id))
.reverse();

expect(expIDs).toEqual(ids);
verifyQuery({
length: 1,
numQueries: 1,
limit: this.limit || DefaultLimit,
});
}

// rawCount isn't affected by filters...
async testRawCount() {
const count = await this.getQuery().queryRawCount();

expect(count).toBe(this.expCount);

verifyCountQuery({ numQueries: 1, length: 1 });
}

async testCount() {
const count = await this.getQuery().queryCount();

expect(count).toBe(this.expCount);

verifyQuery({
length: 1,
numQueries: 1,
limit: this.limit || DefaultLimit,
});
}

async testEdges() {
const edges = await this.getQuery().queryEdges();

expect(edges.length).toBe(this.expCount);

let userCount = 0;
let eventCount = 0;
edges.forEach((edge) => {
if (edge.id2Type === NodeType.FakeEvent) {
eventCount++;
}
if (edge.id2Type === NodeType.FakeUser) {
userCount++;
}
});
expect(userCount).toBe(this.expCount / 2);
expect(eventCount).toBe(this.expCount / 2);
verifyQuery({
length: 1,
numQueries: 1,
limit: this.limit || DefaultLimit,
});
}

async testEnts() {
// privacy...
const ents = await this.getQuery().queryEnts();
expect(ents.length).toBe(this.expCount);

let userCount = 0;
let eventCount = 0;
ents.forEach((ent) => {
if (ent instanceof FakeEvent) {
eventCount++;
}
if (ent instanceof FakeUser) {
userCount++;
}
});
expect(userCount).toBe(this.expCount / 2);
expect(eventCount).toBe(this.expCount / 2);

// when doing privacy checks, hard to say what will be fetched
// verifyQuery({
// // 1 for edges, 1 for users, 1 for events
// length: 3,
// numQueries: 3,
// limit: this.limit || DefaultLimit,
// });
}
}

describe("polymorphic id2s", () => {
const filter = new PolymorphicID2sTestQueryFilter(
(q: UserToFollowingQuery) => {
// no filters
return q;
},
(ents: Ent[]) => {
// nothing to do here
// reverse because edges are most recent first
return ents.reverse();
},
);

// TODO not working when it's a beforeAll
// working with beforeEach but we should only need to create this data once
beforeEach(async () => {
await filter.beforeEach();
});

test("ids", async () => {
await filter.testIDs();
});

test("rawCount", async () => {
await filter.testRawCount();
});

test("count", async () => {
await filter.testCount();
});

test("edges", async () => {
await filter.testEdges();
});

test("ents", async () => {
await filter.testEnts();
});
});
}
6 changes: 4 additions & 2 deletions ts/src/graphql/query/connection_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLString,
} from "graphql";
import { RequestContext } from "../../core/context";
Expand All @@ -13,8 +14,9 @@ import { GraphQLEdgeInterface } from "../builtins/edge";
import { GraphQLConnectionInterface } from "../builtins/connection";
import { Data } from "../../core/base";

type nodeType = GraphQLObjectType | GraphQLInterfaceType;
export class GraphQLEdgeType<
TNode extends GraphQLObjectType,
TNode extends nodeType,
TEdge extends Data,
> extends GraphQLObjectType {
constructor(
Expand Down Expand Up @@ -55,7 +57,7 @@ interface connectionOptions<T extends Data> {
}

export class GraphQLConnectionType<
TNode extends GraphQLObjectType,
TNode extends nodeType,
TEdge extends Data,
> extends GraphQLObjectType {
edgeType: GraphQLEdgeType<TNode, TEdge>;
Expand Down
Loading

0 comments on commit 1a704e4

Please sign in to comment.