@snowtop/ent 0.1.19-test2 → 0.1.20
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/core/base.d.ts +3 -1
- package/core/clause.d.ts +50 -47
- package/core/clause.js +238 -149
- package/core/context.js +1 -1
- package/core/ent.d.ts +2 -3
- package/core/ent.js +27 -17
- package/core/query/custom_clause_query.d.ts +5 -2
- package/core/query/custom_clause_query.js +30 -4
- package/core/query/custom_query.js +1 -0
- package/core/query/query.d.ts +12 -6
- package/core/query/query.js +125 -29
- package/core/query/shared_test.js +20 -7
- package/core/query_impl.d.ts +7 -1
- package/core/query_impl.js +30 -13
- package/graphql/query/shared_edge_connection.js +3 -6
- package/package.json +2 -2
- package/testutils/builder.d.ts +12 -1
- package/testutils/builder.js +35 -2
- package/testutils/db/temp_db.js +1 -1
package/core/context.js
CHANGED
|
@@ -46,7 +46,7 @@ class ContextCache {
|
|
|
46
46
|
parts.push((0, query_impl_1.getOrderByPhrase)(options.orderby));
|
|
47
47
|
}
|
|
48
48
|
if (options.join) {
|
|
49
|
-
parts.push((0, query_impl_1.
|
|
49
|
+
parts.push((0, query_impl_1.getJoinInfo)(options.join).phrase);
|
|
50
50
|
}
|
|
51
51
|
return parts.join(",");
|
|
52
52
|
}
|
package/core/ent.d.ts
CHANGED
|
@@ -132,9 +132,8 @@ export declare class AssocEdge {
|
|
|
132
132
|
}
|
|
133
133
|
export interface cursorOptions {
|
|
134
134
|
row: Data;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
conv?: (any: any) => any;
|
|
135
|
+
keys: string[];
|
|
136
|
+
cursorKeys?: string[];
|
|
138
137
|
}
|
|
139
138
|
export declare function getCursor(opts: cursorOptions): string;
|
|
140
139
|
export declare class AssocEdgeData {
|
package/core/ent.js
CHANGED
|
@@ -848,27 +848,35 @@ class AssocEdge {
|
|
|
848
848
|
getCursor() {
|
|
849
849
|
return getCursor({
|
|
850
850
|
row: this,
|
|
851
|
-
|
|
851
|
+
keys: ["id2"],
|
|
852
852
|
});
|
|
853
853
|
}
|
|
854
854
|
}
|
|
855
855
|
exports.AssocEdge = AssocEdge;
|
|
856
856
|
// TODO eventually update this for sortCol time unique keys
|
|
857
857
|
function getCursor(opts) {
|
|
858
|
-
const { row,
|
|
858
|
+
const { row, keys, cursorKeys } = opts;
|
|
859
859
|
// row: Data, col: string, conv?: (any) => any) {
|
|
860
860
|
if (!row) {
|
|
861
861
|
throw new Error(`no row passed to getCursor`);
|
|
862
862
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
return "";
|
|
863
|
+
if (cursorKeys?.length && cursorKeys.length !== keys.length) {
|
|
864
|
+
throw new Error("length of cursorKeys should match keys");
|
|
866
865
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
866
|
+
const convert = (d) => {
|
|
867
|
+
if (d instanceof Date) {
|
|
868
|
+
return d.getTime();
|
|
869
|
+
}
|
|
870
|
+
return d;
|
|
871
|
+
};
|
|
872
|
+
const parts = [];
|
|
873
|
+
for (let i = 0; i < keys.length; i++) {
|
|
874
|
+
const key = keys[i];
|
|
875
|
+
const cursorKey = cursorKeys?.[i] || key;
|
|
876
|
+
parts.push(key);
|
|
877
|
+
parts.push(convert(row[cursorKey]));
|
|
878
|
+
}
|
|
879
|
+
const str = parts.join(":");
|
|
872
880
|
return Buffer.from(str, "ascii").toString("base64");
|
|
873
881
|
}
|
|
874
882
|
exports.getCursor = getCursor;
|
|
@@ -1078,13 +1086,15 @@ async function loadTwoWayEdges(opts) {
|
|
|
1078
1086
|
fields,
|
|
1079
1087
|
clause: actualClause,
|
|
1080
1088
|
context: opts.context,
|
|
1081
|
-
join:
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1089
|
+
join: [
|
|
1090
|
+
{
|
|
1091
|
+
tableName,
|
|
1092
|
+
alias: "t2",
|
|
1093
|
+
clause: clause.And(
|
|
1094
|
+
// these are not values so need this to not think they're values...
|
|
1095
|
+
clause.Expression("t1.id1 = t2.id2"), clause.Expression("t1.id2 = t2.id1")),
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1088
1098
|
});
|
|
1089
1099
|
return rows;
|
|
1090
1100
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Data, EdgeQueryableDataOptions, Ent, ID, LoadEntOptions, Viewer } from "../base";
|
|
1
|
+
import { Data, EdgeQueryableDataOptions, Ent, ID, LoadEntOptions, QueryDataOptions, Viewer } from "../base";
|
|
2
2
|
import { Clause } from "../clause";
|
|
3
3
|
import { OrderBy } from "../query_impl";
|
|
4
|
-
import { BaseEdgeQuery, IDInfo } from "./query";
|
|
4
|
+
import { BaseEdgeQuery, EdgeQueryOptions, IDInfo } from "./query";
|
|
5
5
|
export interface CustomClauseQueryOptions<TDest extends Ent<TViewer>, TViewer extends Viewer = Viewer> {
|
|
6
6
|
loadEntOptions: LoadEntOptions<TDest, TViewer>;
|
|
7
7
|
clause: Clause;
|
|
@@ -12,6 +12,7 @@ export interface CustomClauseQueryOptions<TDest extends Ent<TViewer>, TViewer ex
|
|
|
12
12
|
orderByDirection?: "ASC" | "DESC";
|
|
13
13
|
nullsPlacement?: "first" | "last";
|
|
14
14
|
disableTransformations?: boolean;
|
|
15
|
+
joinBETA?: QueryDataOptions["join"];
|
|
15
16
|
}
|
|
16
17
|
export declare class CustomClauseQuery<TDest extends Ent<TViewer>, TViewer extends Viewer = Viewer> extends BaseEdgeQuery<any, TDest, Data> {
|
|
17
18
|
viewer: TViewer;
|
|
@@ -20,10 +21,12 @@ export declare class CustomClauseQuery<TDest extends Ent<TViewer>, TViewer exten
|
|
|
20
21
|
constructor(viewer: TViewer, options: CustomClauseQueryOptions<TDest, TViewer>);
|
|
21
22
|
sourceEnt(_id: ID): Promise<null>;
|
|
22
23
|
getTableName(): string;
|
|
24
|
+
protected includeSortColInCursor(options: EdgeQueryOptions): boolean;
|
|
23
25
|
queryRawCount(): Promise<number>;
|
|
24
26
|
queryAllRawCount(): Promise<Map<ID, number>>;
|
|
25
27
|
protected loadRawIDs(_addID: (src: ID) => void): Promise<void>;
|
|
26
28
|
protected loadRawData(_infos: IDInfo[], options: EdgeQueryableDataOptions): Promise<void>;
|
|
27
29
|
dataToID(edge: Data): ID;
|
|
28
30
|
protected loadEntsFromEdges(id: ID, rows: Data[]): Promise<TDest[]>;
|
|
31
|
+
__getOptions(): CustomClauseQueryOptions<TDest, TViewer>;
|
|
29
32
|
}
|
|
@@ -46,6 +46,8 @@ class CustomClauseQuery extends query_1.BaseEdgeQuery {
|
|
|
46
46
|
super(viewer, {
|
|
47
47
|
orderby,
|
|
48
48
|
cursorCol,
|
|
49
|
+
joinBETA: options.joinBETA,
|
|
50
|
+
fieldOptions: options.loadEntOptions,
|
|
49
51
|
});
|
|
50
52
|
this.viewer = viewer;
|
|
51
53
|
this.options = options;
|
|
@@ -57,13 +59,32 @@ class CustomClauseQuery extends query_1.BaseEdgeQuery {
|
|
|
57
59
|
getTableName() {
|
|
58
60
|
return this.options.loadEntOptions.tableName;
|
|
59
61
|
}
|
|
62
|
+
includeSortColInCursor(options) {
|
|
63
|
+
// TODO maybe we should just always do this?
|
|
64
|
+
return options.joinBETA !== undefined && this.sortCol !== this.cursorCol;
|
|
65
|
+
}
|
|
60
66
|
async queryRawCount() {
|
|
67
|
+
// sqlite needs as count otherwise it returns count(1)
|
|
68
|
+
let fields = ["count(1) as count"];
|
|
69
|
+
if (this.options.joinBETA) {
|
|
70
|
+
const requestedFields = this.options.loadEntOptions.fields;
|
|
71
|
+
const alias = this.options.loadEntOptions.fieldsAlias ??
|
|
72
|
+
this.options.loadEntOptions.alias;
|
|
73
|
+
if (alias) {
|
|
74
|
+
fields = [`count(distinct ${alias}.${requestedFields[0]}) as count`];
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fields = [`count(distinct ${requestedFields[0]}) as count`];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
61
80
|
const row = await (0, ent_1.loadRow)({
|
|
81
|
+
...this.options.loadEntOptions,
|
|
62
82
|
tableName: this.options.loadEntOptions.tableName,
|
|
63
|
-
|
|
64
|
-
fields: ["count(1) as count"],
|
|
83
|
+
fields,
|
|
65
84
|
clause: this.clause,
|
|
66
85
|
context: this.viewer.context,
|
|
86
|
+
join: this.options.joinBETA,
|
|
87
|
+
disableFieldsAlias: true,
|
|
67
88
|
});
|
|
68
89
|
return parseInt(row?.count, 10) || 0;
|
|
69
90
|
}
|
|
@@ -86,12 +107,14 @@ class CustomClauseQuery extends query_1.BaseEdgeQuery {
|
|
|
86
107
|
options.limit = (0, ent_1.getDefaultLimit)();
|
|
87
108
|
}
|
|
88
109
|
const rows = await (0, ent_1.loadRows)({
|
|
89
|
-
|
|
90
|
-
fields: this.options.loadEntOptions.fields,
|
|
110
|
+
...this.options.loadEntOptions,
|
|
91
111
|
clause: (0, clause_1.AndOptional)(this.clause, options.clause),
|
|
92
112
|
orderby: options.orderby,
|
|
93
113
|
limit: options?.limit || (0, ent_1.getDefaultLimit)(),
|
|
94
114
|
context: this.viewer.context,
|
|
115
|
+
join: this.options.joinBETA,
|
|
116
|
+
// if doing a join, select distinct rows
|
|
117
|
+
distinct: this.options.joinBETA !== undefined,
|
|
95
118
|
});
|
|
96
119
|
this.edges.set(1, rows);
|
|
97
120
|
}
|
|
@@ -101,5 +124,8 @@ class CustomClauseQuery extends query_1.BaseEdgeQuery {
|
|
|
101
124
|
async loadEntsFromEdges(id, rows) {
|
|
102
125
|
return (0, ent_1.applyPrivacyPolicyForRows)(this.viewer, rows, this.options.loadEntOptions);
|
|
103
126
|
}
|
|
127
|
+
__getOptions() {
|
|
128
|
+
return this.options;
|
|
129
|
+
}
|
|
104
130
|
}
|
|
105
131
|
exports.CustomClauseQuery = CustomClauseQuery;
|
package/core/query/query.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ID, Ent, Viewer, EdgeQueryableDataOptions, Data, PrivacyPolicy, EdgeQueryableDataOptionsConfigureLoader } from "../base";
|
|
1
|
+
import { ID, Ent, Viewer, EdgeQueryableDataOptions, Data, PrivacyPolicy, EdgeQueryableDataOptionsConfigureLoader, QueryableDataOptions, SelectBaseDataOptions } from "../base";
|
|
2
2
|
import { OrderBy } from "../query_impl";
|
|
3
3
|
export interface EdgeQuery<TSource extends Ent, TDest extends Ent, TEdge extends Data> {
|
|
4
4
|
queryEdges(): Promise<TEdge[]>;
|
|
@@ -31,9 +31,12 @@ export interface PaginationInfo {
|
|
|
31
31
|
startCursor: string;
|
|
32
32
|
endCursor: string;
|
|
33
33
|
}
|
|
34
|
-
interface EdgeQueryOptions {
|
|
34
|
+
export interface EdgeQueryOptions {
|
|
35
35
|
cursorCol: string;
|
|
36
|
+
cursorColIsDate?: boolean;
|
|
36
37
|
orderby: OrderBy;
|
|
38
|
+
joinBETA?: NonNullable<QueryableDataOptions["join"]>;
|
|
39
|
+
fieldOptions?: SelectBaseDataOptions;
|
|
37
40
|
}
|
|
38
41
|
export declare abstract class BaseEdgeQuery<TSource extends Ent, TDest extends Ent, TEdge extends Data> implements EdgeQuery<TSource, TDest, TEdge> {
|
|
39
42
|
viewer: Viewer;
|
|
@@ -45,10 +48,13 @@ export declare abstract class BaseEdgeQuery<TSource extends Ent, TDest extends E
|
|
|
45
48
|
protected genIDInfosToFetch: () => Promise<IDInfo[]>;
|
|
46
49
|
private idMap;
|
|
47
50
|
private idsToFetch;
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
protected sortCol: string;
|
|
52
|
+
protected cursorCol: string;
|
|
53
|
+
protected cursorColIsDate: boolean;
|
|
54
|
+
protected sortColIsDate: boolean;
|
|
50
55
|
private edgeQueryOptions;
|
|
51
56
|
private limitAdded;
|
|
57
|
+
private cursorKeys;
|
|
52
58
|
constructor(viewer: Viewer, sortCol: string, cursorCol: string);
|
|
53
59
|
constructor(viewer: Viewer, options: EdgeQueryOptions);
|
|
54
60
|
protected getSortCol(): string;
|
|
@@ -81,12 +87,12 @@ export declare abstract class BaseEdgeQuery<TSource extends Ent, TDest extends E
|
|
|
81
87
|
protected genIDInfosToFetchImpl(): Promise<IDInfo[]>;
|
|
82
88
|
private _defaultEdgeQueryableOptions;
|
|
83
89
|
protected configureEdgeQueryableDataOptions(opts: EdgeQueryableDataOptionsConfigureLoader): void;
|
|
84
|
-
protected getDefaultEdgeQueryOptions(): Partial<Pick<
|
|
90
|
+
protected getDefaultEdgeQueryOptions(): Partial<Pick<QueryableDataOptions, "clause" | "limit" | "orderby" | "disableTransformations">> | undefined;
|
|
85
91
|
private loadEdges;
|
|
92
|
+
protected includeSortColInCursor(options: EdgeQueryOptions): boolean;
|
|
86
93
|
getCursor(row: TEdge): string;
|
|
87
94
|
}
|
|
88
95
|
export interface IDInfo {
|
|
89
96
|
id: ID;
|
|
90
97
|
invalidated?: boolean;
|
|
91
98
|
}
|
|
92
|
-
export {};
|
package/core/query/query.js
CHANGED
|
@@ -40,36 +40,61 @@ function assertPositive(n) {
|
|
|
40
40
|
throw new Error("cannot use a negative number");
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
function
|
|
44
|
-
let decoded = Buffer.from(cursor, "base64").toString("ascii");
|
|
45
|
-
let parts = decoded.split(":");
|
|
46
|
-
// uuid, don't parse int since it tries to validate just first part
|
|
47
|
-
if ((0, uuid_1.validate)(parts[1])) {
|
|
48
|
-
return parts[1];
|
|
49
|
-
}
|
|
50
|
-
// invalid or unknown cursor. nothing to do here.
|
|
51
|
-
if (parts.length !== 2 || parts[0] !== col) {
|
|
52
|
-
throw new Error(`invalid cursor ${cursor} passed`);
|
|
53
|
-
}
|
|
43
|
+
function convertToIntMaybe(val) {
|
|
54
44
|
// TODO handle both cases... (time vs not) better
|
|
55
45
|
// TODO change this to only do the parseInt part if time...
|
|
56
46
|
// pass flag indicating if time?
|
|
57
|
-
|
|
47
|
+
// handle non-integers for which the first part is an int
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
if (isNaN(val)) {
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
const time = parseInt(val, 10);
|
|
58
53
|
if (isNaN(time)) {
|
|
59
|
-
return
|
|
54
|
+
return val;
|
|
60
55
|
}
|
|
61
56
|
return time;
|
|
62
57
|
}
|
|
58
|
+
function assertValidCursor(cursor, opts) {
|
|
59
|
+
let decoded = Buffer.from(cursor, "base64").toString("ascii");
|
|
60
|
+
let parts = decoded.split(":");
|
|
61
|
+
const { keys } = opts;
|
|
62
|
+
// invalid or unknown cursor. nothing to do here.
|
|
63
|
+
// we should have the same number of parts as keys * 2
|
|
64
|
+
if (parts.length !== keys.length * 2) {
|
|
65
|
+
throw new Error(`invalid cursor ${cursor} passed`);
|
|
66
|
+
}
|
|
67
|
+
const values = [];
|
|
68
|
+
for (let i = 0; i < keys.length; i++) {
|
|
69
|
+
const key = keys[i];
|
|
70
|
+
const keyPart = parts[i * 2];
|
|
71
|
+
if (key !== keyPart) {
|
|
72
|
+
throw new Error(`invalid cursor ${cursor} passed. expected ${key}. got ${keyPart} as key of field`);
|
|
73
|
+
}
|
|
74
|
+
const val = parts[i * 2 + 1];
|
|
75
|
+
// uuid, don't parse int since it tries to validate just first part
|
|
76
|
+
if ((0, uuid_1.validate)(val)) {
|
|
77
|
+
values.push(val);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
values.push(convertToIntMaybe(val));
|
|
81
|
+
}
|
|
82
|
+
return values;
|
|
83
|
+
}
|
|
63
84
|
const orderbyRegex = new RegExp(/([0-9a-z_]+)[ ]?([0-9a-z_]+)?/i);
|
|
64
85
|
class FirstFilter {
|
|
65
86
|
constructor(options) {
|
|
66
87
|
this.options = options;
|
|
67
88
|
this.pageMap = new Map();
|
|
68
89
|
this.usedQuery = false;
|
|
90
|
+
this.cursorValues = [];
|
|
69
91
|
assertPositive(options.limit);
|
|
70
92
|
this.sortCol = options.sortCol;
|
|
71
93
|
if (options.after) {
|
|
72
|
-
this.
|
|
94
|
+
this.cursorValues = assertValidCursor(options.after, {
|
|
95
|
+
keys: options.cursorKeys,
|
|
96
|
+
});
|
|
97
|
+
this.offset = this.cursorValues[0];
|
|
73
98
|
}
|
|
74
99
|
this.edgeQuery = options.query;
|
|
75
100
|
}
|
|
@@ -133,6 +158,18 @@ class FirstFilter {
|
|
|
133
158
|
// so we'd need a way to indicate whether this is done in sql or not
|
|
134
159
|
return edges;
|
|
135
160
|
}
|
|
161
|
+
getOffsetForQuery() {
|
|
162
|
+
// cursorCol maps to offset which we get from the cursor in assertValidCursor
|
|
163
|
+
return this.options.cursorColIsDate
|
|
164
|
+
? new Date(this.offset).toISOString()
|
|
165
|
+
: this.offset;
|
|
166
|
+
}
|
|
167
|
+
getSortValueForQuery() {
|
|
168
|
+
// sortCol maps to the value we're comparing against
|
|
169
|
+
return this.options.sortColIsDate
|
|
170
|
+
? new Date(this.cursorValues[1]).toISOString()
|
|
171
|
+
: this.cursorValues[1];
|
|
172
|
+
}
|
|
136
173
|
async query(options) {
|
|
137
174
|
this.usedQuery = true;
|
|
138
175
|
// we fetch an extra one to see if we're at the end
|
|
@@ -150,17 +187,25 @@ class FirstFilter {
|
|
|
150
187
|
});
|
|
151
188
|
if (this.offset) {
|
|
152
189
|
const res = this.edgeQuery.getTableName();
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
190
|
+
let tableName = (0, types_1.isPromise)(res) ? await res : res;
|
|
191
|
+
// using a join, we already know sortCol and cursorCol are different
|
|
192
|
+
// we have encoded both values in the cursor
|
|
193
|
+
// includeSortColInCursor() is true in this case
|
|
194
|
+
if (this.cursorValues.length === 2 &&
|
|
195
|
+
this.options.cursorKeys.length === 2) {
|
|
196
|
+
options.clause = clause.AndOptional(options.clause, clause.PaginationMultipleColsQuery(this.sortCol, this.options.cursorCol, less, this.getSortValueForQuery(), this.getOffsetForQuery(), this.options.fieldOptions?.fieldsAlias ??
|
|
197
|
+
this.options.fieldOptions?.alias));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// inner col time
|
|
201
|
+
options.clause = clause.AndOptional(options.clause, clause.PaginationMultipleColsSubQuery(this.sortCol, less ? "<" : ">", tableName, this.options.cursorCol, this.getOffsetForQuery()));
|
|
202
|
+
}
|
|
156
203
|
}
|
|
157
204
|
}
|
|
158
205
|
else {
|
|
159
206
|
if (this.offset) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
? new Date(this.offset).toISOString()
|
|
163
|
-
: this.offset;
|
|
207
|
+
const clauseFn = less ? clause.Less : clause.Greater;
|
|
208
|
+
const val = this.getOffsetForQuery();
|
|
164
209
|
options.clause = clause.AndOptional(options.clause, clauseFn(this.sortCol, val));
|
|
165
210
|
}
|
|
166
211
|
}
|
|
@@ -172,14 +217,20 @@ class FirstFilter {
|
|
|
172
217
|
return this.pageMap.get(id);
|
|
173
218
|
}
|
|
174
219
|
}
|
|
220
|
+
// TODO LastFilter same behavior as FirstFilter
|
|
221
|
+
// TODO can we share so we don't keep needing to change in both
|
|
175
222
|
class LastFilter {
|
|
176
223
|
constructor(options) {
|
|
177
224
|
this.options = options;
|
|
178
225
|
this.pageMap = new Map();
|
|
226
|
+
this.cursorValues = [];
|
|
179
227
|
assertPositive(options.limit);
|
|
180
228
|
this.sortCol = options.sortCol;
|
|
181
229
|
if (options.before) {
|
|
182
|
-
this.
|
|
230
|
+
this.cursorValues = assertValidCursor(options.before, {
|
|
231
|
+
keys: options.cursorKeys,
|
|
232
|
+
});
|
|
233
|
+
this.offset = this.cursorValues[0];
|
|
183
234
|
}
|
|
184
235
|
this.edgeQuery = options.query;
|
|
185
236
|
}
|
|
@@ -210,6 +261,19 @@ class LastFilter {
|
|
|
210
261
|
}
|
|
211
262
|
return ret;
|
|
212
263
|
}
|
|
264
|
+
// copied from FirstFilter
|
|
265
|
+
getOffsetForQuery() {
|
|
266
|
+
// cursorCol maps to offset which we get from the cursor in assertValidCursor
|
|
267
|
+
return this.options.cursorColIsDate
|
|
268
|
+
? new Date(this.offset).toISOString()
|
|
269
|
+
: this.offset;
|
|
270
|
+
}
|
|
271
|
+
getSortValueForQuery() {
|
|
272
|
+
// sortCol maps to the value we're comparing against
|
|
273
|
+
return this.options.sortColIsDate
|
|
274
|
+
? new Date(this.cursorValues[1]).toISOString()
|
|
275
|
+
: this.cursorValues[1];
|
|
276
|
+
}
|
|
213
277
|
async query(options) {
|
|
214
278
|
const orderby = (0, query_impl_1.reverseOrderBy)(this.options.orderby);
|
|
215
279
|
const greater = orderby[0].direction === "ASC";
|
|
@@ -218,8 +282,20 @@ class LastFilter {
|
|
|
218
282
|
const res = this.edgeQuery.getTableName();
|
|
219
283
|
const tableName = (0, types_1.isPromise)(res) ? await res : res;
|
|
220
284
|
if (this.offset) {
|
|
221
|
-
//
|
|
222
|
-
|
|
285
|
+
// using a join, we already know sortCol and cursorCol are different
|
|
286
|
+
// we have encoded both values in the cursor
|
|
287
|
+
// includeSortColInCursor() is true in this case
|
|
288
|
+
if (this.cursorValues.length === 2 &&
|
|
289
|
+
this.options.cursorKeys.length === 2) {
|
|
290
|
+
options.clause = clause.AndOptional(options.clause, clause.PaginationMultipleColsQuery(this.sortCol, this.options.cursorCol,
|
|
291
|
+
// flipped here since we're going in the opposite direction
|
|
292
|
+
!greater, this.getSortValueForQuery(), this.getOffsetForQuery(), this.options.fieldOptions?.fieldsAlias ??
|
|
293
|
+
this.options.fieldOptions?.alias));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// inner col time
|
|
297
|
+
options.clause = clause.AndOptional(options.clause, clause.PaginationMultipleColsSubQuery(this.sortCol, greater ? ">" : "<", tableName, this.options.cursorCol, this.getOffsetForQuery()));
|
|
298
|
+
}
|
|
223
299
|
}
|
|
224
300
|
// we also sort cursor col in same direction. (direction doesn't matter)
|
|
225
301
|
orderby.push({
|
|
@@ -229,10 +305,8 @@ class LastFilter {
|
|
|
229
305
|
}
|
|
230
306
|
else {
|
|
231
307
|
if (this.offset) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
? new Date(this.offset).toISOString()
|
|
235
|
-
: this.offset;
|
|
308
|
+
const clauseFn = greater ? clause.Greater : clause.Less;
|
|
309
|
+
const val = this.getOffsetForQuery();
|
|
236
310
|
options.clause = clause.AndOptional(options.clause, clauseFn(this.sortCol, val));
|
|
237
311
|
}
|
|
238
312
|
}
|
|
@@ -252,6 +326,7 @@ class BaseEdgeQuery {
|
|
|
252
326
|
this.idMap = new Map();
|
|
253
327
|
this.idsToFetch = [];
|
|
254
328
|
this.limitAdded = false;
|
|
329
|
+
this.cursorKeys = [];
|
|
255
330
|
// this is basically just raw rows
|
|
256
331
|
this.queryEdges = async () => {
|
|
257
332
|
return this.querySingleEdge("queryEdges");
|
|
@@ -304,6 +379,8 @@ class BaseEdgeQuery {
|
|
|
304
379
|
};
|
|
305
380
|
let sortCol;
|
|
306
381
|
let cursorCol;
|
|
382
|
+
let sortColIsDate = false;
|
|
383
|
+
let cursorColIsDate = false;
|
|
307
384
|
if (typeof sortColOrOptions === "string") {
|
|
308
385
|
sortCol = sortColOrOptions;
|
|
309
386
|
cursorCol = cursorColMaybe;
|
|
@@ -324,11 +401,15 @@ class BaseEdgeQuery {
|
|
|
324
401
|
else {
|
|
325
402
|
// TODO this orderby isn't consistent and this logic needs to be changed anywhere that's using this and this.getSortCol()
|
|
326
403
|
sortCol = sortColOrOptions.orderby[0].column;
|
|
404
|
+
sortColIsDate = sortColOrOptions.orderby[0].dateColumn ?? false;
|
|
327
405
|
}
|
|
328
406
|
cursorCol = sortColOrOptions.cursorCol;
|
|
407
|
+
cursorColIsDate = sortColOrOptions.cursorColIsDate ?? false;
|
|
329
408
|
this.edgeQueryOptions = sortColOrOptions;
|
|
330
409
|
}
|
|
331
410
|
this.sortCol = sortCol;
|
|
411
|
+
this.sortColIsDate = sortColIsDate;
|
|
412
|
+
this.cursorColIsDate = cursorColIsDate;
|
|
332
413
|
let m = orderbyRegex.exec(sortCol);
|
|
333
414
|
if (!m) {
|
|
334
415
|
throw new Error(`invalid sort column ${sortCol}`);
|
|
@@ -340,6 +421,10 @@ class BaseEdgeQuery {
|
|
|
340
421
|
this.cursorCol = cursorCol;
|
|
341
422
|
this.memoizedloadEdges = (0, memoizee_1.default)(this.loadEdges.bind(this));
|
|
342
423
|
this.genIDInfosToFetch = (0, memoizee_1.default)(this.genIDInfosToFetchImpl.bind(this));
|
|
424
|
+
this.cursorKeys.push(this.cursorCol);
|
|
425
|
+
if (this.includeSortColInCursor(this.edgeQueryOptions)) {
|
|
426
|
+
this.cursorKeys.push(this.sortCol);
|
|
427
|
+
}
|
|
343
428
|
}
|
|
344
429
|
getSortCol() {
|
|
345
430
|
return this.sortCol;
|
|
@@ -356,8 +441,12 @@ class BaseEdgeQuery {
|
|
|
356
441
|
after,
|
|
357
442
|
sortCol: this.sortCol,
|
|
358
443
|
cursorCol: this.cursorCol,
|
|
444
|
+
cursorKeys: this.cursorKeys,
|
|
445
|
+
cursorColIsDate: this.cursorColIsDate,
|
|
446
|
+
sortColIsDate: this.sortColIsDate,
|
|
359
447
|
orderby: this.edgeQueryOptions.orderby,
|
|
360
448
|
query: this,
|
|
449
|
+
fieldOptions: this.edgeQueryOptions.fieldOptions,
|
|
361
450
|
}));
|
|
362
451
|
return this;
|
|
363
452
|
}
|
|
@@ -379,8 +468,12 @@ class BaseEdgeQuery {
|
|
|
379
468
|
before,
|
|
380
469
|
sortCol: this.sortCol,
|
|
381
470
|
cursorCol: this.cursorCol,
|
|
471
|
+
cursorKeys: this.cursorKeys,
|
|
472
|
+
cursorColIsDate: this.cursorColIsDate,
|
|
473
|
+
sortColIsDate: this.sortColIsDate,
|
|
382
474
|
orderby: this.edgeQueryOptions.orderby,
|
|
383
475
|
query: this,
|
|
476
|
+
fieldOptions: this.edgeQueryOptions.fieldOptions,
|
|
384
477
|
}));
|
|
385
478
|
return this;
|
|
386
479
|
}
|
|
@@ -490,10 +583,13 @@ class BaseEdgeQuery {
|
|
|
490
583
|
this.queryDispatched = true;
|
|
491
584
|
return this.edges;
|
|
492
585
|
}
|
|
586
|
+
includeSortColInCursor(options) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
493
589
|
getCursor(row) {
|
|
494
590
|
return (0, ent_1.getCursor)({
|
|
495
591
|
row,
|
|
496
|
-
|
|
592
|
+
keys: this.cursorKeys,
|
|
497
593
|
});
|
|
498
594
|
}
|
|
499
595
|
}
|
|
@@ -205,11 +205,24 @@ const commonTests = (opts) => {
|
|
|
205
205
|
function getViewer() {
|
|
206
206
|
return new viewer_1.LoggedOutViewer();
|
|
207
207
|
}
|
|
208
|
-
function getCursorFrom(contacts, idx) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
function getCursorFrom(q, contacts, idx) {
|
|
209
|
+
let opts;
|
|
210
|
+
if (isCustomQuery(q)) {
|
|
211
|
+
opts = {
|
|
212
|
+
row: contacts[idx],
|
|
213
|
+
keys: ["id"],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// for assoc queries, we're getting the value from 'id' field but the edge
|
|
218
|
+
// is from assoc_edge table id2 field and so cursor takes it from there
|
|
219
|
+
opts = {
|
|
220
|
+
row: contacts[idx],
|
|
221
|
+
keys: ["id2"],
|
|
222
|
+
cursorKeys: ["id"],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return (0, ent_1.getCursor)(opts);
|
|
213
226
|
}
|
|
214
227
|
function getVerifyAfterEachCursor(edges, pageLength, user) {
|
|
215
228
|
return (0, query_1.getVerifyAfterEachCursorGeneric)(edges, pageLength, user,
|
|
@@ -550,7 +563,7 @@ const commonTests = (opts) => {
|
|
|
550
563
|
const idx = 2;
|
|
551
564
|
const N = 3;
|
|
552
565
|
const filter = new TestQueryFilter((q, user, contacts) => {
|
|
553
|
-
return q.first(N, getCursorFrom(contacts, idx));
|
|
566
|
+
return q.first(N, getCursorFrom(q, contacts, idx));
|
|
554
567
|
}, opts.newQuery, (contacts) => {
|
|
555
568
|
if (opts.orderby[0].direction === "DESC") {
|
|
556
569
|
// < check so we shouldn't get that index
|
|
@@ -611,7 +624,7 @@ const commonTests = (opts) => {
|
|
|
611
624
|
const idx = 2;
|
|
612
625
|
const N = 3;
|
|
613
626
|
const filter = new TestQueryFilter((q, user, contacts) => {
|
|
614
|
-
return q.last(N, getCursorFrom(contacts, idx));
|
|
627
|
+
return q.last(N, getCursorFrom(q, contacts, idx));
|
|
615
628
|
}, opts.newQuery, (contacts) => {
|
|
616
629
|
// > check so we don't want that index
|
|
617
630
|
if (opts.orderby[0].direction === "DESC") {
|
package/core/query_impl.d.ts
CHANGED
|
@@ -3,9 +3,15 @@ export interface OrderByOption {
|
|
|
3
3
|
column: string;
|
|
4
4
|
direction: "ASC" | "DESC";
|
|
5
5
|
nullsPlacement?: "first" | "last";
|
|
6
|
+
dateColumn?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export type OrderBy = OrderByOption[];
|
|
8
9
|
export declare function getOrderByPhrase(orderby: OrderBy, alias?: string): string;
|
|
9
10
|
export declare function reverseOrderBy(orderby: OrderBy): OrderBy;
|
|
10
|
-
|
|
11
|
+
interface JoinInfo {
|
|
12
|
+
phrase: string;
|
|
13
|
+
valuesUsed: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function getJoinInfo(join: NonNullable<QueryableDataOptions["join"]>, clauseIdx?: number): JoinInfo;
|
|
11
16
|
export declare function buildQuery(options: QueryableDataOptions): string;
|
|
17
|
+
export {};
|