@snowtop/ent 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action/experimental_action.js +2 -2
- package/action/operations.js +4 -2
- package/core/base.d.ts +12 -10
- package/core/clause.d.ts +4 -3
- package/core/clause.js +98 -43
- package/core/config.d.ts +6 -0
- package/core/context.d.ts +6 -14
- package/core/context.js +9 -4
- package/core/db.js +1 -1
- package/core/ent.d.ts +1 -0
- package/core/ent.js +30 -11
- package/core/loaders/assoc_count_loader.js +1 -1
- package/core/loaders/assoc_edge_loader.d.ts +3 -0
- package/core/loaders/assoc_edge_loader.js +18 -0
- package/core/loaders/object_loader.js +2 -2
- package/core/loaders/query_loader.d.ts +3 -3
- package/core/loaders/query_loader.js +2 -1
- package/core/loaders/raw_count_loader.js +1 -1
- package/core/query/assoc_query.d.ts +22 -0
- package/core/query/assoc_query.js +101 -2
- package/core/query/custom_query.js +2 -9
- package/core/query/query.d.ts +1 -0
- package/core/query/query.js +72 -11
- package/core/query/shared_assoc_test.js +404 -7
- package/core/query/shared_test.js +9 -37
- package/core/query_impl.d.ts +2 -1
- package/core/query_impl.js +25 -7
- package/graphql/query/edge_connection.js +2 -2
- package/package.json +2 -2
- package/parse_schema/parse.d.ts +4 -3
- package/parse_schema/parse.js +4 -3
- package/schema/base_schema.d.ts +3 -0
- package/schema/base_schema.js +2 -0
- package/schema/schema.d.ts +1 -0
- package/schema/struct_field.d.ts +4 -2
- package/schema/struct_field.js +33 -4
- package/scripts/custom_graphql.js +1 -1
- package/testutils/builder.d.ts +1 -1
- package/testutils/builder.js +4 -4
- package/testutils/ent-graphql-tests/index.js +2 -2
- package/testutils/fake_data/fake_contact.js +1 -1
- package/testutils/fake_data/fake_event.js +1 -1
- package/testutils/fake_data/test_helpers.js +2 -2
- package/testutils/fake_data/user_query.js +1 -1
- package/testutils/query.d.ts +9 -0
- package/testutils/query.js +45 -0
- package/testutils/write.js +3 -3
|
@@ -12,8 +12,14 @@ const builder_1 = require("../../testutils/builder");
|
|
|
12
12
|
const luxon_1 = require("luxon");
|
|
13
13
|
const convert_1 = require("../convert");
|
|
14
14
|
const test_context_1 = require("../../testutils/context/test_context");
|
|
15
|
+
const query_1 = require("../../testutils/query");
|
|
15
16
|
function assocTests(ml, global = false) {
|
|
16
17
|
ml.mock();
|
|
18
|
+
// beforeAll(async () => {
|
|
19
|
+
// // TODO this is needed when we do only in a test here
|
|
20
|
+
// // need to figure this out
|
|
21
|
+
// await createEdges();
|
|
22
|
+
// });
|
|
17
23
|
describe("custom edge", () => {
|
|
18
24
|
let user1, user2;
|
|
19
25
|
beforeEach(async () => {
|
|
@@ -58,12 +64,12 @@ function assocTests(ml, global = false) {
|
|
|
58
64
|
return execArray?.[3];
|
|
59
65
|
}
|
|
60
66
|
function verifyQuery({ length = 1, numQueries = 1, limit = (0, ent_1.getDefaultLimit)(), extraClause = undefined, disablePaginationBump = false, logsStart = 0, direction = "DESC", }) {
|
|
61
|
-
const
|
|
67
|
+
const clauses = [(0, clause_1.Eq)("id1", ""), (0, clause_1.Eq)("edge_type", "")];
|
|
62
68
|
if (extraClause) {
|
|
63
|
-
|
|
69
|
+
clauses.push(extraClause);
|
|
64
70
|
}
|
|
65
71
|
if (global) {
|
|
66
|
-
|
|
72
|
+
clauses.push((0, clause_1.Eq)("deleted_at", null));
|
|
67
73
|
}
|
|
68
74
|
expect(ml.logs.length).toBe(length);
|
|
69
75
|
for (let i = logsStart; i < numQueries; i++) {
|
|
@@ -71,7 +77,7 @@ function assocTests(ml, global = false) {
|
|
|
71
77
|
let expLimit = disablePaginationBump ? limit : limit + 1;
|
|
72
78
|
expect(whereClause, `${i}`).toBe(
|
|
73
79
|
// default limit
|
|
74
|
-
`${(0, clause_1.And)(...
|
|
80
|
+
`${(0, clause_1.And)(...clauses).clause(1)} ORDER BY time ${direction}, id2 ${direction} LIMIT ${expLimit}`);
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
function verifyCountQuery({ length = 1, numQueries = 1 }) {
|
|
@@ -132,7 +138,7 @@ function assocTests(ml, global = false) {
|
|
|
132
138
|
expect(countMap.size).toBe(this.dataz.length);
|
|
133
139
|
for (let i = 0; i < this.dataz.length; i++) {
|
|
134
140
|
let data = this.dataz[i];
|
|
135
|
-
expect(countMap.get(data[0].id)).
|
|
141
|
+
expect(countMap.get(data[0].id)).toBe(test_helpers_1.inputs.length);
|
|
136
142
|
}
|
|
137
143
|
verifyCountQuery({ numQueries: 3, length: 3 });
|
|
138
144
|
}
|
|
@@ -141,7 +147,7 @@ function assocTests(ml, global = false) {
|
|
|
141
147
|
expect(countMap.size).toBe(this.dataz.length);
|
|
142
148
|
for (let i = 0; i < this.dataz.length; i++) {
|
|
143
149
|
let data = this.dataz[i];
|
|
144
|
-
expect(countMap.get(data[0].id)).
|
|
150
|
+
expect(countMap.get(data[0].id)).toBe(data[1].length);
|
|
145
151
|
}
|
|
146
152
|
verifyQuery({
|
|
147
153
|
length: this.dataz.length,
|
|
@@ -260,6 +266,34 @@ function assocTests(ml, global = false) {
|
|
|
260
266
|
await filter.testEnts();
|
|
261
267
|
});
|
|
262
268
|
});
|
|
269
|
+
function validateQueryIntersectOrUnion(ml, user1, user2) {
|
|
270
|
+
return function (_query, _cursor) {
|
|
271
|
+
// 2 queries for user because privacy
|
|
272
|
+
// 2 queries to fetch edges.
|
|
273
|
+
expect(ml.logs.length).toBe(4);
|
|
274
|
+
const where1 = getWhereClause(ml.logs[1]);
|
|
275
|
+
const clauses1 = [
|
|
276
|
+
(0, clause_1.Eq)("id1", user1.id),
|
|
277
|
+
(0, clause_1.Eq)("edge_type", index_1.EdgeType.UserToFriends),
|
|
278
|
+
];
|
|
279
|
+
const clauses2 = [
|
|
280
|
+
(0, clause_1.Eq)("id1", user2.id),
|
|
281
|
+
(0, clause_1.Eq)("edge_type", index_1.EdgeType.UserToFriends),
|
|
282
|
+
];
|
|
283
|
+
if (global) {
|
|
284
|
+
clauses1.push((0, clause_1.Eq)("deleted_at", null));
|
|
285
|
+
clauses2.push((0, clause_1.Eq)("deleted_at", null));
|
|
286
|
+
}
|
|
287
|
+
const clause1 = (0, clause_1.And)(...clauses1);
|
|
288
|
+
expect(where1).toBe(`${clause1.clause(1)} ORDER BY time DESC LIMIT 1000`);
|
|
289
|
+
expect(ml.logs[1].values).toStrictEqual(clause1.values());
|
|
290
|
+
const where2 = getWhereClause(ml.logs[3]);
|
|
291
|
+
const clause2 = (0, clause_1.And)(...clauses2);
|
|
292
|
+
// TODO 1001 vs 1000 here
|
|
293
|
+
expect(where2).toBe(`${clause2.clause(1)} ORDER BY time DESC, id2 DESC LIMIT 1001`);
|
|
294
|
+
expect(ml.logs[3].values).toStrictEqual(clause2.values());
|
|
295
|
+
};
|
|
296
|
+
}
|
|
263
297
|
class ChainTestQueryFilter {
|
|
264
298
|
constructor(initialQuery, subsequentQueries, filter, lastHopFilter) {
|
|
265
299
|
this.initialQuery = initialQuery;
|
|
@@ -288,7 +322,7 @@ function assocTests(ml, global = false) {
|
|
|
288
322
|
builder.orchestrator.addInboundEdge(this.event2.id, index_1.EdgeType.EventToAttendees, index_1.NodeType.FakeEvent);
|
|
289
323
|
}
|
|
290
324
|
await builder.saveX();
|
|
291
|
-
return
|
|
325
|
+
return builder.editedEntX();
|
|
292
326
|
}));
|
|
293
327
|
expect(this.friends.length).toBe(test_helpers_1.inputs.length);
|
|
294
328
|
const count = await index_1.UserToFriendsQuery.query(new viewer_1.IDViewer(this.user.id), this.user.id).queryCount();
|
|
@@ -948,6 +982,369 @@ function assocTests(ml, global = false) {
|
|
|
948
982
|
expect(info2.hasPreviousPage).toBe(false);
|
|
949
983
|
});
|
|
950
984
|
});
|
|
985
|
+
describe("intersect", () => {
|
|
986
|
+
let users = [];
|
|
987
|
+
let user1;
|
|
988
|
+
let user2;
|
|
989
|
+
beforeEach(async () => {
|
|
990
|
+
users = [];
|
|
991
|
+
for (let i = 0; i < 10; i++) {
|
|
992
|
+
const user = await (0, test_helpers_1.createTestUser)();
|
|
993
|
+
users.push(user);
|
|
994
|
+
}
|
|
995
|
+
for (let i = 0; i < 10; i++) {
|
|
996
|
+
const user = users[i];
|
|
997
|
+
// decreasing number of friends for each user
|
|
998
|
+
const candidates = users
|
|
999
|
+
.slice(0, 10 - i)
|
|
1000
|
+
.filter((u) => u.id != user.id);
|
|
1001
|
+
await (0, test_helpers_1.addEdge)(user, index_1.FakeUserSchema, index_1.EdgeType.UserToFriends, false, ...candidates);
|
|
1002
|
+
const count = await index_1.UserToFriendsQuery.query(user.viewer, user).queryRawCount();
|
|
1003
|
+
expect(count, `${i}`).toBe(candidates.length);
|
|
1004
|
+
}
|
|
1005
|
+
user1 = users[0];
|
|
1006
|
+
user2 = users[1];
|
|
1007
|
+
});
|
|
1008
|
+
function getQuery() {
|
|
1009
|
+
return index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__intersect(index_1.UserToFriendsQuery.query(user2.viewer, user2.id));
|
|
1010
|
+
}
|
|
1011
|
+
function getCandidateIDs() {
|
|
1012
|
+
// not the first 2 since that's user1 and user2
|
|
1013
|
+
// not the last one since that's removed from user2's list of friends
|
|
1014
|
+
return users.slice(2, users.length - 1).map((u) => u.id);
|
|
1015
|
+
}
|
|
1016
|
+
test("ids", async () => {
|
|
1017
|
+
const ids = await getQuery().queryIDs();
|
|
1018
|
+
const candidates = getCandidateIDs();
|
|
1019
|
+
expect(ids.length).toBe(candidates.length);
|
|
1020
|
+
expect(ids.sort()).toEqual(candidates.sort());
|
|
1021
|
+
});
|
|
1022
|
+
test("count", async () => {
|
|
1023
|
+
const count = await getQuery().queryCount();
|
|
1024
|
+
const candidates = getCandidateIDs();
|
|
1025
|
+
expect(count).toBe(candidates.length);
|
|
1026
|
+
});
|
|
1027
|
+
test("raw_count", async () => {
|
|
1028
|
+
const count = await getQuery().queryRawCount();
|
|
1029
|
+
// raw count doesn't include the intersection
|
|
1030
|
+
expect(count).toBe(users.length - 1);
|
|
1031
|
+
});
|
|
1032
|
+
test("edges", async () => {
|
|
1033
|
+
const edges = await getQuery().queryEdges();
|
|
1034
|
+
const candidates = getCandidateIDs();
|
|
1035
|
+
expect(edges.length).toBe(candidates.length);
|
|
1036
|
+
// for an intersect, the edge returned is always for the source id
|
|
1037
|
+
for (const edge of edges) {
|
|
1038
|
+
expect(edge.id1).toBe(user1.id);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
test("ents", async () => {
|
|
1042
|
+
const ents = await getQuery().queryEnts();
|
|
1043
|
+
const candidates = getCandidateIDs().sort();
|
|
1044
|
+
expect(ents.length).toBe(candidates.length);
|
|
1045
|
+
expect(ents.map((u) => u.id).sort()).toEqual(candidates.sort());
|
|
1046
|
+
});
|
|
1047
|
+
test("first", async () => {
|
|
1048
|
+
const ents = await getQuery().first(2).queryEnts();
|
|
1049
|
+
expect(ents.length).toBe(2);
|
|
1050
|
+
});
|
|
1051
|
+
test("first. after each cursor", async () => {
|
|
1052
|
+
const edges = await getQuery().queryEdges();
|
|
1053
|
+
const { verify, getCursor } = (0, query_1.getVerifyAfterEachCursorGeneric)(edges, 2, user1, getQuery, ml, validateQueryIntersectOrUnion(ml, user1, user2));
|
|
1054
|
+
// this one intentionally not generic so we know where to stop...
|
|
1055
|
+
expect(edges.length).toBe(7);
|
|
1056
|
+
await verify(0, true, true, undefined);
|
|
1057
|
+
await verify(2, true, true, getCursor(edges[1]));
|
|
1058
|
+
await verify(4, true, true, getCursor(edges[3]));
|
|
1059
|
+
// 1 item, no nextPage
|
|
1060
|
+
await verify(6, true, false, getCursor(edges[5]));
|
|
1061
|
+
await verify(7, false, false, getCursor(edges[6]));
|
|
1062
|
+
});
|
|
1063
|
+
test("multiple intersections", async () => {
|
|
1064
|
+
const query = index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__intersect(index_1.UserToFriendsQuery.query(user2.viewer, user2.id), index_1.UserToFriendsQuery.query(user2.viewer, users[2].id), index_1.UserToFriendsQuery.query(user2.viewer, users[3].id));
|
|
1065
|
+
// user0 => 0-9 - self
|
|
1066
|
+
// user1 => 0-8 - self
|
|
1067
|
+
// user2 => 0-7 - self
|
|
1068
|
+
// user3 => 0-6 -self
|
|
1069
|
+
// 7 users - 4 = 3 since you can't be friends with yourself
|
|
1070
|
+
const candidates = [users[4], users[5], users[6]];
|
|
1071
|
+
const [ids, count, edges, ents] = await Promise.all([
|
|
1072
|
+
query.queryIDs(),
|
|
1073
|
+
query.queryCount(),
|
|
1074
|
+
query.queryEdges(),
|
|
1075
|
+
query.queryEnts(),
|
|
1076
|
+
]);
|
|
1077
|
+
expect(ids.length).toBe(candidates.length);
|
|
1078
|
+
expect(count).toBe(candidates.length);
|
|
1079
|
+
expect(edges.length).toBe(candidates.length);
|
|
1080
|
+
expect(ents.length).toBe(candidates.length);
|
|
1081
|
+
expect(edges.map((e) => e.id2).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1082
|
+
expect(ents.map((e) => e.id).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1083
|
+
});
|
|
1084
|
+
test("multiple sources", async () => {
|
|
1085
|
+
const query = index_1.UserToFriendsQuery.query(user1.viewer, [
|
|
1086
|
+
user1.id,
|
|
1087
|
+
user2.id,
|
|
1088
|
+
]).__intersect(index_1.UserToFriendsQuery.query(user2.viewer, users[2].id), index_1.UserToFriendsQuery.query(user2.viewer, users[3].id));
|
|
1089
|
+
// user0 => 0-9 - self
|
|
1090
|
+
// user1 => 0-8 - self
|
|
1091
|
+
// user2 => 0-7 - self
|
|
1092
|
+
// user3 => 0-6 -self
|
|
1093
|
+
// 7 users - 3 = 4 since you can't be friends with yourself
|
|
1094
|
+
// subtracting 3 for each instead of 4 like above because user1 not intersecting with user2
|
|
1095
|
+
const candidates1 = [users[1], users[4], users[5], users[6]];
|
|
1096
|
+
const candidates2 = [users[0], users[4], users[5], users[6]];
|
|
1097
|
+
const [idsMap, countMap, edgesMap, entsMap] = await Promise.all([
|
|
1098
|
+
query.queryAllIDs(),
|
|
1099
|
+
query.queryAllCount(),
|
|
1100
|
+
query.queryAllEdges(),
|
|
1101
|
+
query.queryAllEnts(),
|
|
1102
|
+
]);
|
|
1103
|
+
async function verify(source, candidates) {
|
|
1104
|
+
const ids = idsMap.get(source.id) ?? [];
|
|
1105
|
+
const count = countMap.get(source.id) ?? [];
|
|
1106
|
+
const edges = edgesMap.get(source.id) ?? [];
|
|
1107
|
+
const ents = entsMap.get(source.id) ?? [];
|
|
1108
|
+
expect(ids.length).toBe(candidates.length);
|
|
1109
|
+
expect(count).toBe(candidates.length);
|
|
1110
|
+
expect(edges.length).toBe(candidates.length);
|
|
1111
|
+
expect(ents.length).toBe(candidates.length);
|
|
1112
|
+
expect(edges.map((e) => e.id2).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1113
|
+
expect(ents.map((e) => e.id).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1114
|
+
}
|
|
1115
|
+
await verify(user1, candidates1);
|
|
1116
|
+
await verify(user2, candidates2);
|
|
1117
|
+
});
|
|
1118
|
+
test("different edge types", async () => {
|
|
1119
|
+
const event = await (0, test_helpers_1.createTestEvent)(user2);
|
|
1120
|
+
for (let i = 0; i < 5; i++) {
|
|
1121
|
+
const newUser = await (0, test_helpers_1.createTestUser)();
|
|
1122
|
+
await (0, test_helpers_1.addEdge)(event, index_1.FakeEventSchema, index_1.EdgeType.EventToAttendees, false, newUser);
|
|
1123
|
+
}
|
|
1124
|
+
const query = index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__intersect(index_1.EventToAttendeesQuery.query(user1.viewer, event.id));
|
|
1125
|
+
const count = await query.queryCount();
|
|
1126
|
+
expect(count).toBe(0);
|
|
1127
|
+
const candidates = [];
|
|
1128
|
+
for (let i = 0; i < users.length; i++) {
|
|
1129
|
+
const user = users[i];
|
|
1130
|
+
if (user.id !== user1.id && user.id !== user2.id) {
|
|
1131
|
+
await (0, test_helpers_1.addEdge)(event, index_1.FakeEventSchema, index_1.EdgeType.EventToAttendees, false, user);
|
|
1132
|
+
candidates.push(user);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const query2 = index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__intersect(index_1.EventToAttendeesQuery.query(user1.viewer, event.id));
|
|
1136
|
+
const count2 = await query2.queryCount();
|
|
1137
|
+
const ents = await query2.queryEnts();
|
|
1138
|
+
const ids = await query2.queryIDs();
|
|
1139
|
+
expect(count2).toBe(candidates.length);
|
|
1140
|
+
expect(ids.length).toBe(candidates.length);
|
|
1141
|
+
expect(ents.length).toBe(candidates.length);
|
|
1142
|
+
expect(ents.map((e) => e.id).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
describe("union", () => {
|
|
1146
|
+
let users = [];
|
|
1147
|
+
let user1;
|
|
1148
|
+
let user2;
|
|
1149
|
+
let friends = new Map();
|
|
1150
|
+
beforeEach(async () => {
|
|
1151
|
+
users = [];
|
|
1152
|
+
friends = new Map();
|
|
1153
|
+
for (let i = 0; i < 10; i++) {
|
|
1154
|
+
const user = await (0, test_helpers_1.createTestUser)();
|
|
1155
|
+
users.push(user);
|
|
1156
|
+
}
|
|
1157
|
+
for (let i = 0; i < 10; i++) {
|
|
1158
|
+
const user = users[i];
|
|
1159
|
+
// decreasing number of friends for each user
|
|
1160
|
+
// we want little overlap so new users created.
|
|
1161
|
+
const candidates = users
|
|
1162
|
+
.slice(0, 10 - i)
|
|
1163
|
+
.filter((u) => u.id != user.id);
|
|
1164
|
+
// add at least 5 more to each user
|
|
1165
|
+
// every user should end up with 14 or 15
|
|
1166
|
+
// 9 + 5 = 14
|
|
1167
|
+
// 8 + 6 = 14
|
|
1168
|
+
// 7 + 7 = 14
|
|
1169
|
+
// 6 + 8 = 14
|
|
1170
|
+
// 5 + 9 = 14
|
|
1171
|
+
// 4 + 10 = 14
|
|
1172
|
+
// 3 + 11 = 14
|
|
1173
|
+
// 2 + 12 = 14
|
|
1174
|
+
// 1 + 13 = 14
|
|
1175
|
+
// 0 + 14 = 14
|
|
1176
|
+
// 10 original
|
|
1177
|
+
// 5 new for friend 1
|
|
1178
|
+
// 6 new for friend 2
|
|
1179
|
+
const newFriends = await Promise.all(new Array(5 + i).fill(null).map(() => (0, test_helpers_1.createTestUser)()));
|
|
1180
|
+
candidates.push(...newFriends);
|
|
1181
|
+
await (0, test_helpers_1.addEdge)(user, index_1.FakeUserSchema, index_1.EdgeType.UserToFriends, false, ...candidates);
|
|
1182
|
+
const count = await index_1.UserToFriendsQuery.query(user.viewer, user).queryRawCount();
|
|
1183
|
+
expect(count, `${i}`).toBe(candidates.length);
|
|
1184
|
+
friends.set(user.id, candidates);
|
|
1185
|
+
}
|
|
1186
|
+
user1 = users[0];
|
|
1187
|
+
user2 = users[1];
|
|
1188
|
+
});
|
|
1189
|
+
function getQuery() {
|
|
1190
|
+
return index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__union(index_1.UserToFriendsQuery.query(user2.viewer, user2.id));
|
|
1191
|
+
}
|
|
1192
|
+
function getCandidateIDs() {
|
|
1193
|
+
const set = new Set();
|
|
1194
|
+
friends.get(user1.id)?.forEach((u) => set.add(u));
|
|
1195
|
+
friends.get(user2.id)?.forEach((u) => set.add(u));
|
|
1196
|
+
return Array.from(set.values()).map((u) => u.id);
|
|
1197
|
+
}
|
|
1198
|
+
test("ids", async () => {
|
|
1199
|
+
const ids = await getQuery().queryIDs();
|
|
1200
|
+
const candidates = getCandidateIDs();
|
|
1201
|
+
expect(ids.length).toBe(candidates.length);
|
|
1202
|
+
expect(ids.sort()).toEqual(candidates.sort());
|
|
1203
|
+
});
|
|
1204
|
+
test("count", async () => {
|
|
1205
|
+
const count = await getQuery().queryCount();
|
|
1206
|
+
const candidates = getCandidateIDs();
|
|
1207
|
+
expect(count).toBe(candidates.length);
|
|
1208
|
+
});
|
|
1209
|
+
test("raw_count", async () => {
|
|
1210
|
+
const count = await getQuery().queryRawCount();
|
|
1211
|
+
// raw count doesn't include the intersection
|
|
1212
|
+
expect(count).toBe(friends.get(user1.id)?.length);
|
|
1213
|
+
});
|
|
1214
|
+
test("edges", async () => {
|
|
1215
|
+
const edges = await getQuery().queryEdges();
|
|
1216
|
+
const candidates = getCandidateIDs();
|
|
1217
|
+
expect(edges.length).toBe(candidates.length);
|
|
1218
|
+
const idMap = new Map();
|
|
1219
|
+
for (const edge of edges) {
|
|
1220
|
+
const ct = (idMap.get(edge.id1) ?? 0) + 1;
|
|
1221
|
+
idMap.set(edge.id1, ct);
|
|
1222
|
+
}
|
|
1223
|
+
let id1count = friends.get(user1.id)?.length ?? 0;
|
|
1224
|
+
expect(idMap.get(user1.id)).toBe(id1count);
|
|
1225
|
+
expect(idMap.get(user2.id)).toBe(edges.length - id1count);
|
|
1226
|
+
});
|
|
1227
|
+
test("ents", async () => {
|
|
1228
|
+
// only returns privacy aware which is friends + self...
|
|
1229
|
+
const ents = await getQuery().queryEnts();
|
|
1230
|
+
const candidates = getCandidateIDs().sort();
|
|
1231
|
+
let id1count = friends.get(user1.id)?.length ?? 0;
|
|
1232
|
+
const visible = id1count + 1;
|
|
1233
|
+
expect(ents.length).toBe(visible);
|
|
1234
|
+
});
|
|
1235
|
+
test("first", async () => {
|
|
1236
|
+
const ents = await getQuery().first(2).queryEnts();
|
|
1237
|
+
expect(ents.length).toBe(2);
|
|
1238
|
+
});
|
|
1239
|
+
test("first. after each cursor", async () => {
|
|
1240
|
+
const edges = await getQuery().queryEdges();
|
|
1241
|
+
const { verify, getCursor } = (0, query_1.getVerifyAfterEachCursorGeneric)(edges, 3, user1, getQuery, ml, validateQueryIntersectOrUnion(ml, user1, user2));
|
|
1242
|
+
// hardcoded to test
|
|
1243
|
+
expect(edges.length).toBe(21);
|
|
1244
|
+
await verify(0, true, true, undefined);
|
|
1245
|
+
await verify(3, true, true, getCursor(edges[2]));
|
|
1246
|
+
await verify(6, true, true, getCursor(edges[5]));
|
|
1247
|
+
await verify(9, true, true, getCursor(edges[8]));
|
|
1248
|
+
await verify(12, true, true, getCursor(edges[11]));
|
|
1249
|
+
await verify(15, true, true, getCursor(edges[14]));
|
|
1250
|
+
await verify(18, true, true, getCursor(edges[17]));
|
|
1251
|
+
await verify(21, false, false, getCursor(edges[20]));
|
|
1252
|
+
});
|
|
1253
|
+
test("multiple unions", async () => {
|
|
1254
|
+
const user3 = users[2];
|
|
1255
|
+
const user4 = users[3];
|
|
1256
|
+
const query = index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__union(index_1.UserToFriendsQuery.query(user1.viewer, user2.id), index_1.UserToFriendsQuery.query(user1.viewer, user3.id), index_1.UserToFriendsQuery.query(user1.viewer, user4.id));
|
|
1257
|
+
const candidates = Array.from(new Set([
|
|
1258
|
+
...(friends.get(user1.id) ?? []),
|
|
1259
|
+
...(friends.get(user2.id) ?? []),
|
|
1260
|
+
...(friends.get(user3.id) ?? []),
|
|
1261
|
+
...(friends.get(user4.id) ?? []), // 0-9 (-self) + 8 friends
|
|
1262
|
+
]).values());
|
|
1263
|
+
// user1 can only see self + friends
|
|
1264
|
+
const user1Visible = friends.get(user1.id) ?? [];
|
|
1265
|
+
user1Visible.push(user1);
|
|
1266
|
+
// should be 36
|
|
1267
|
+
expect(candidates.length).toBe(10 + 5 + 6 + 7 + 8);
|
|
1268
|
+
const [ids, count, edges, ents] = await Promise.all([
|
|
1269
|
+
query.queryIDs(),
|
|
1270
|
+
query.queryCount(),
|
|
1271
|
+
query.queryEdges(),
|
|
1272
|
+
query.queryEnts(),
|
|
1273
|
+
]);
|
|
1274
|
+
expect(ids.length).toBe(candidates.length);
|
|
1275
|
+
expect(count).toBe(candidates.length);
|
|
1276
|
+
expect(edges.length).toBe(candidates.length);
|
|
1277
|
+
expect(ents.length).toBe(user1Visible.length);
|
|
1278
|
+
expect(edges.map((e) => e.id2).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1279
|
+
expect(ents.map((e) => e.id).sort()).toEqual(user1Visible.map((u) => u.id).sort());
|
|
1280
|
+
});
|
|
1281
|
+
test("multiple sources", async () => {
|
|
1282
|
+
const user3 = users[2];
|
|
1283
|
+
const user4 = users[3];
|
|
1284
|
+
const query = index_1.UserToFriendsQuery.query(user1.viewer, [
|
|
1285
|
+
user1.id,
|
|
1286
|
+
user2.id,
|
|
1287
|
+
]).__union(index_1.UserToFriendsQuery.query(user2.viewer, user3.id), index_1.UserToFriendsQuery.query(user2.viewer, user4.id));
|
|
1288
|
+
const candidates1 = Array.from(new Set([
|
|
1289
|
+
...(friends.get(user1.id) ?? []),
|
|
1290
|
+
...(friends.get(user3.id) ?? []),
|
|
1291
|
+
...(friends.get(user4.id) ?? []), // 0-9 (-self) + 8 friends
|
|
1292
|
+
]).values());
|
|
1293
|
+
const candidates2 = Array.from(new Set([
|
|
1294
|
+
...(friends.get(user2.id) ?? []),
|
|
1295
|
+
...(friends.get(user3.id) ?? []),
|
|
1296
|
+
...(friends.get(user4.id) ?? []), // 0-9 (-self) + 8 friends
|
|
1297
|
+
]).values());
|
|
1298
|
+
const [idsMap, countMap, edgesMap, entsMap] = await Promise.all([
|
|
1299
|
+
query.queryAllIDs(),
|
|
1300
|
+
query.queryAllCount(),
|
|
1301
|
+
query.queryAllEdges(),
|
|
1302
|
+
query.queryAllEnts(),
|
|
1303
|
+
]);
|
|
1304
|
+
// user1 can only see self + friends
|
|
1305
|
+
const user1Visible = friends.get(user1.id) ?? [];
|
|
1306
|
+
user1Visible.push(user1);
|
|
1307
|
+
// since the EntQuery uses user1's viewer
|
|
1308
|
+
// can only see user2's friends that intersect with user1's friends
|
|
1309
|
+
// and that's every user except for the last user since we don't add that user to user2's friends
|
|
1310
|
+
const user2Visible = users.slice(0, users.length - 1);
|
|
1311
|
+
async function verify(source, candidates, visible) {
|
|
1312
|
+
const ids = idsMap.get(source.id) ?? [];
|
|
1313
|
+
const count = countMap.get(source.id) ?? [];
|
|
1314
|
+
const edges = edgesMap.get(source.id) ?? [];
|
|
1315
|
+
const ents = entsMap.get(source.id) ?? [];
|
|
1316
|
+
expect(ids.length).toBe(candidates.length);
|
|
1317
|
+
expect(count).toBe(candidates.length);
|
|
1318
|
+
expect(edges.length).toBe(candidates.length);
|
|
1319
|
+
expect(ents.length).toBe(visible.length);
|
|
1320
|
+
expect(edges.map((e) => e.id2).sort()).toEqual(candidates.map((u) => u.id).sort());
|
|
1321
|
+
expect(ents.map((e) => e.id).sort()).toEqual(visible.map((u) => u.id).sort());
|
|
1322
|
+
}
|
|
1323
|
+
await verify(user1, candidates1, user1Visible);
|
|
1324
|
+
await verify(user2, candidates2, user2Visible);
|
|
1325
|
+
});
|
|
1326
|
+
test("different edge types", async () => {
|
|
1327
|
+
const event = await (0, test_helpers_1.createTestEvent)(user2);
|
|
1328
|
+
const newUsers = [];
|
|
1329
|
+
for (let i = 0; i < 5; i++) {
|
|
1330
|
+
const newUser = await (0, test_helpers_1.createTestUser)();
|
|
1331
|
+
await (0, test_helpers_1.addEdge)(event, index_1.FakeEventSchema, index_1.EdgeType.EventToAttendees, false, newUser);
|
|
1332
|
+
newUsers.push(newUser);
|
|
1333
|
+
}
|
|
1334
|
+
const query = index_1.UserToFriendsQuery.query(user1.viewer, user1.id).__union(index_1.EventToAttendeesQuery.query(user1.viewer, event.id));
|
|
1335
|
+
const candidates = [
|
|
1336
|
+
...(friends.get(user1.id) ?? []).map((u) => u.id),
|
|
1337
|
+
...newUsers.map((u) => u.id),
|
|
1338
|
+
];
|
|
1339
|
+
const count = await query.queryCount();
|
|
1340
|
+
expect(count).toBe(candidates.length);
|
|
1341
|
+
const ents = await query.queryEnts();
|
|
1342
|
+
const ids = await query.queryIDs();
|
|
1343
|
+
expect(ids.length).toBe(candidates.length);
|
|
1344
|
+
// can only see friends
|
|
1345
|
+
expect(ents.length).toBe(friends.get(user1.id)?.length);
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
951
1348
|
if (!global) {
|
|
952
1349
|
return;
|
|
953
1350
|
}
|
|
@@ -14,13 +14,7 @@ const builder_1 = require("../../testutils/builder");
|
|
|
14
14
|
const action_1 = require("../../action");
|
|
15
15
|
const clause_1 = require("../clause");
|
|
16
16
|
const query_impl_1 = require("../query_impl");
|
|
17
|
-
|
|
18
|
-
const idx = query.query.indexOf("WHERE");
|
|
19
|
-
if (idx !== -1) {
|
|
20
|
-
return query.query.substr(idx + 6);
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
17
|
+
const query_1 = require("../../testutils/query");
|
|
24
18
|
const commonTests = (opts) => {
|
|
25
19
|
(0, logger_1.setLogLevels)(["query", "error"]);
|
|
26
20
|
const ml = opts.ml;
|
|
@@ -152,7 +146,7 @@ const commonTests = (opts) => {
|
|
|
152
146
|
const uniqCol = isCustomQuery(filter) ? "id" : "id2";
|
|
153
147
|
expect(ml.logs.length).toBe(length);
|
|
154
148
|
for (let i = 0; i < numQueries; i++) {
|
|
155
|
-
const whereClause = getWhereClause(ml.logs[i]);
|
|
149
|
+
const whereClause = (0, query_1.getWhereClause)(ml.logs[i]);
|
|
156
150
|
let expLimit = disablePaginationBump ? limit : limit + 1;
|
|
157
151
|
expect(whereClause, `${i}`).toBe(
|
|
158
152
|
// default limit
|
|
@@ -162,7 +156,7 @@ const commonTests = (opts) => {
|
|
|
162
156
|
function verifyCountQuery({ length = 1, numQueries = 1 }) {
|
|
163
157
|
expect(ml.logs.length).toBe(length);
|
|
164
158
|
for (let i = 0; i < numQueries; i++) {
|
|
165
|
-
const whereClause = getWhereClause(ml.logs[i]);
|
|
159
|
+
const whereClause = (0, query_1.getWhereClause)(ml.logs[i]);
|
|
166
160
|
expect(whereClause).toBe(opts.clause.clause(1));
|
|
167
161
|
}
|
|
168
162
|
}
|
|
@@ -180,7 +174,7 @@ const commonTests = (opts) => {
|
|
|
180
174
|
else {
|
|
181
175
|
parts.push(cmp);
|
|
182
176
|
}
|
|
183
|
-
expect(getWhereClause(ml.logs[0])).toBe(`${parts.join(" AND ")} ORDER BY ${(0, query_impl_1.getOrderByPhrase)(opts.orderby)}, ${uniqCol} ${opts.orderby[0].direction} LIMIT ${limit + 1}`);
|
|
177
|
+
expect((0, query_1.getWhereClause)(ml.logs[0])).toBe(`${parts.join(" AND ")} ORDER BY ${(0, query_impl_1.getOrderByPhrase)(opts.orderby)}, ${uniqCol} ${opts.orderby[0].direction} LIMIT ${limit + 1}`);
|
|
184
178
|
}
|
|
185
179
|
function verifyLastBeforeCursorQuery(filter, { length = 1, limit = 3, orderby = opts.orderby }) {
|
|
186
180
|
// cache showing up in a few because of cross runs...
|
|
@@ -196,7 +190,7 @@ const commonTests = (opts) => {
|
|
|
196
190
|
else {
|
|
197
191
|
parts.push(cmp);
|
|
198
192
|
}
|
|
199
|
-
expect(getWhereClause(ml.logs[0])).toBe(
|
|
193
|
+
expect((0, query_1.getWhereClause)(ml.logs[0])).toBe(
|
|
200
194
|
// extra fetched for pagination
|
|
201
195
|
`${parts.join(" AND ")} ORDER BY ${(0, query_impl_1.getOrderByPhrase)(orderby)}, ${uniqCol} ${orderby[0].direction} LIMIT ${limit + 1}`);
|
|
202
196
|
}
|
|
@@ -210,38 +204,16 @@ const commonTests = (opts) => {
|
|
|
210
204
|
});
|
|
211
205
|
}
|
|
212
206
|
function getVerifyAfterEachCursor(edges, pageLength, user) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
query = opts.newQuery(getViewer(), user);
|
|
217
|
-
const newEdges = await query.first(pageLength, cursor).queryEdges();
|
|
218
|
-
const pagination = query.paginationInfo().get(user.id);
|
|
219
|
-
if (hasEdge) {
|
|
220
|
-
expect(newEdges[0], `${i}`).toStrictEqual(edges[i]);
|
|
221
|
-
expect(newEdges.length, `${i}`).toBe(edges.length - i >= pageLength ? pageLength : edges.length - i);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
expect(newEdges.length, `${i}`).toBe(0);
|
|
225
|
-
}
|
|
226
|
-
if (hasNextPage) {
|
|
227
|
-
expect(pagination?.hasNextPage, `${i}`).toBe(true);
|
|
228
|
-
expect(pagination?.hasPreviousPage, `${i}`).toBe(false);
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
expect(pagination?.hasNextPage, `${i}`).toBe(undefined);
|
|
232
|
-
expect(pagination?.hasNextPage, `${i}`).toBe(undefined);
|
|
233
|
-
}
|
|
207
|
+
return (0, query_1.getVerifyAfterEachCursorGeneric)(edges, pageLength, user,
|
|
208
|
+
// @ts-ignore weirdness with import paths...
|
|
209
|
+
() => opts.newQuery(getViewer(), user), ml, (query, cursor) => {
|
|
234
210
|
if (cursor) {
|
|
235
211
|
verifyFirstAfterCursorQuery(query, 1, pageLength);
|
|
236
212
|
}
|
|
237
213
|
else {
|
|
238
214
|
verifyQuery(query, { orderby: opts.orderby, limit: pageLength });
|
|
239
215
|
}
|
|
240
|
-
}
|
|
241
|
-
function getCursor(edge) {
|
|
242
|
-
return query.getCursor(edge);
|
|
243
|
-
}
|
|
244
|
-
return { verify, getCursor };
|
|
216
|
+
});
|
|
245
217
|
}
|
|
246
218
|
function getVerifyBeforeEachCursor(edges, pageLength, user) {
|
|
247
219
|
let query;
|
package/core/query_impl.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface OrderByOption {
|
|
|
5
5
|
nullsPlacement?: "first" | "last";
|
|
6
6
|
}
|
|
7
7
|
export type OrderBy = OrderByOption[];
|
|
8
|
-
export declare function getOrderByPhrase(orderby: OrderBy): string;
|
|
8
|
+
export declare function getOrderByPhrase(orderby: OrderBy, alias?: string): string;
|
|
9
9
|
export declare function reverseOrderBy(orderby: OrderBy): OrderBy;
|
|
10
|
+
export declare function getJoinPhrase(join: NonNullable<QueryableDataOptions["join"]>, clauseIdx?: number): string;
|
|
10
11
|
export declare function buildQuery(options: QueryableDataOptions): string;
|
package/core/query_impl.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildQuery = exports.reverseOrderBy = exports.getOrderByPhrase = void 0;
|
|
4
|
-
function getOrderByPhrase(orderby) {
|
|
3
|
+
exports.buildQuery = exports.getJoinPhrase = exports.reverseOrderBy = exports.getOrderByPhrase = void 0;
|
|
4
|
+
function getOrderByPhrase(orderby, alias) {
|
|
5
5
|
return orderby
|
|
6
6
|
.map((v) => {
|
|
7
7
|
let nullsPlacement = "";
|
|
@@ -13,7 +13,8 @@ function getOrderByPhrase(orderby) {
|
|
|
13
13
|
nullsPlacement = " NULLS LAST";
|
|
14
14
|
break;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
const col = alias ? `${alias}.${v.column}` : v.column;
|
|
17
|
+
return `${col} ${v.direction}${nullsPlacement}`;
|
|
17
18
|
})
|
|
18
19
|
.join(", ");
|
|
19
20
|
}
|
|
@@ -26,17 +27,34 @@ function reverseOrderBy(orderby) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
exports.reverseOrderBy = reverseOrderBy;
|
|
30
|
+
function getJoinPhrase(join, clauseIdx = 1) {
|
|
31
|
+
const joinTable = join.alias
|
|
32
|
+
? `${join.tableName} ${join.alias}`
|
|
33
|
+
: join.tableName;
|
|
34
|
+
return `${joinTable} ON ${join.clause.clause(clauseIdx)}`;
|
|
35
|
+
}
|
|
36
|
+
exports.getJoinPhrase = getJoinPhrase;
|
|
29
37
|
function buildQuery(options) {
|
|
30
|
-
const fields = options.
|
|
38
|
+
const fields = options.alias
|
|
39
|
+
? options.fields.map((f) => `${options.alias}.${f}`).join(", ")
|
|
40
|
+
: options.fields.join(", ");
|
|
31
41
|
// always start at 1
|
|
32
|
-
const whereClause = options.clause.clause(1);
|
|
33
42
|
const parts = [];
|
|
34
|
-
|
|
43
|
+
const tableName = options.alias
|
|
44
|
+
? `${options.tableName} AS ${options.alias}`
|
|
45
|
+
: options.tableName;
|
|
46
|
+
parts.push(`SELECT ${fields} FROM ${tableName}`);
|
|
47
|
+
let whereStart = 1;
|
|
48
|
+
if (options.join) {
|
|
49
|
+
parts.push(`JOIN ${getJoinPhrase(options.join, 1)}`);
|
|
50
|
+
whereStart += options.join.clause.values().length;
|
|
51
|
+
}
|
|
52
|
+
parts.push(`WHERE ${options.clause.clause(whereStart, options.alias)}`);
|
|
35
53
|
if (options.groupby) {
|
|
36
54
|
parts.push(`GROUP BY ${options.groupby}`);
|
|
37
55
|
}
|
|
38
56
|
if (options.orderby) {
|
|
39
|
-
parts.push(`ORDER BY ${getOrderByPhrase(options.orderby)}`);
|
|
57
|
+
parts.push(`ORDER BY ${getOrderByPhrase(options.orderby, options.alias)}`);
|
|
40
58
|
}
|
|
41
59
|
if (options.limit) {
|
|
42
60
|
parts.push(`LIMIT ${options.limit}`);
|
|
@@ -49,7 +49,7 @@ class GraphQLEdgeConnection {
|
|
|
49
49
|
this.query = fn(this.query);
|
|
50
50
|
}
|
|
51
51
|
async queryTotalCount() {
|
|
52
|
-
return
|
|
52
|
+
return this.query.queryRawCount();
|
|
53
53
|
}
|
|
54
54
|
async queryEdges() {
|
|
55
55
|
// because of privacy, we need to query the node regardless of if the node is there
|
|
@@ -60,7 +60,7 @@ class GraphQLEdgeConnection {
|
|
|
60
60
|
// if nodes queried just return ents
|
|
61
61
|
// unlikely to query nodes and pageInfo so we just load this separately for now
|
|
62
62
|
async queryNodes() {
|
|
63
|
-
return
|
|
63
|
+
return this.query.queryEnts();
|
|
64
64
|
}
|
|
65
65
|
defaultPageInfo() {
|
|
66
66
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowtop/ent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "snowtop ent framework",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"minimist": "^1.2.8",
|
|
22
22
|
"pascal-case": "^3.1.2",
|
|
23
23
|
"pg": "^8.11.0",
|
|
24
|
-
"prettier": "^
|
|
24
|
+
"prettier": "^3.0.3",
|
|
25
25
|
"snake-case": "^3.0.4",
|
|
26
26
|
"ts-node": "^10.9.1",
|
|
27
27
|
"tsconfig-paths": "^4.2.0",
|