@getstrata/core 2.2.2 → 2.2.4
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/CHANGELOG.md +8 -0
- package/dist/core/database/baseRepository.d.ts +2 -0
- package/dist/core/database/keyset.d.ts +7 -0
- package/dist/core/database/model.d.ts +14 -1
- package/dist/core/database/repositoryQuery.d.ts +9 -1
- package/dist/core/database/types.d.ts +5 -0
- package/dist/core/database/whereBuilder.d.ts +7 -0
- package/dist/core/pagination/index.d.ts +21 -0
- package/dist/entries/database/query.js +12 -0
- package/dist/entries/database/repositoryQuery.js +26 -0
- package/dist/framework/public-api.d.ts +1 -1
- package/dist/index.js +156 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KeysetCursor, KeysetOptions } from "../pagination/index.ts";
|
|
1
2
|
import { type CursorPaginatedResult, type PaginatedResult } from "../pagination/index.ts";
|
|
2
3
|
import { type BelongsToManyRelation, type BelongsToRelation, type HasManyRelation, type HasManyThroughRelation, type MorphManyRelation, type MorphOneRelation, type MorphToRelation } from "./relationships.ts";
|
|
3
4
|
import { RepositoryQuery } from "./repositoryQuery.ts";
|
|
@@ -37,6 +38,7 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
|
|
|
37
38
|
cursorColumn?: PrimaryKey;
|
|
38
39
|
direction?: "asc" | "desc";
|
|
39
40
|
} & Omit<ExtendedQueryOptions<TEntity>, "limit" | "offset">): Promise<CursorPaginatedResult<TEntity, TEntity[PrimaryKey]>>;
|
|
41
|
+
keysetPaginate(options: KeysetOptions<TEntity> & Omit<ExtendedQueryOptions<TEntity>, "orderBy" | "limit" | "offset">): Promise<CursorPaginatedResult<TEntity, KeysetCursor>>;
|
|
40
42
|
findById(id: TEntity[PrimaryKey]): Promise<TEntity | null>;
|
|
41
43
|
findByIdOrThrow(id: TEntity[PrimaryKey], errorFactory?: ErrorFactory<TEntity[PrimaryKey]>): Promise<TEntity>;
|
|
42
44
|
findByIds(ids: readonly TEntity[PrimaryKey][]): Promise<TEntity[]>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KeysetCursor, KeysetOptions } from "../pagination/index.ts";
|
|
2
|
+
import type { TableDefinition } from "./table.ts";
|
|
3
|
+
import type { WhereNode } from "./whereBuilder.ts";
|
|
4
|
+
declare const KEYSET_ALIAS_PREFIX = "__strata_keyset_";
|
|
5
|
+
declare function validateKeyset<TEntity extends object, PK extends keyof TEntity & string>(table: TableDefinition<TEntity, PK>, options: KeysetOptions<TEntity>): KeysetCursor["order"];
|
|
6
|
+
declare function keysetBoundary<TEntity extends object>(order: KeysetCursor["order"], cursor?: KeysetCursor): WhereNode<TEntity>[];
|
|
7
|
+
export { KEYSET_ALIAS_PREFIX, keysetBoundary, validateKeyset };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CursorPaginatedResult, KeysetCursor, KeysetOptions } from "../pagination/index.ts";
|
|
1
2
|
import BaseRepository from "./baseRepository.ts";
|
|
2
3
|
import type { AnyRelationQuery, RelatedModelClass } from "./relationQuery.ts";
|
|
3
4
|
import { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuery, HasManyThroughRelationQuery, HasOneRelationQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery } from "./relationQuery.ts";
|
|
@@ -55,7 +56,9 @@ declare function applyCasts(values: LoadedAttributes, casts: ModelCasts, directi
|
|
|
55
56
|
/** Read-only plain records; partial selection never acquires model mutation methods. */
|
|
56
57
|
declare class ModelProjection<TEntity extends object, K extends keyof TEntity & string> {
|
|
57
58
|
private readonly load;
|
|
58
|
-
|
|
59
|
+
private readonly page;
|
|
60
|
+
constructor(load: (limit?: number) => Promise<Array<Pick<TEntity, K>>>, page: (options: KeysetOptions<TEntity>) => Promise<CursorPaginatedResult<Pick<TEntity, K>, KeysetCursor>>);
|
|
61
|
+
keysetPaginate(options: KeysetOptions<TEntity>): Promise<CursorPaginatedResult<Pick<TEntity, K>, KeysetCursor>>;
|
|
59
62
|
get(): Promise<Array<Pick<TEntity, K>>>;
|
|
60
63
|
first(): Promise<Pick<TEntity, K> | null>;
|
|
61
64
|
}
|
|
@@ -106,6 +109,10 @@ declare class ModelQuery<TModel extends object = AnyModel, TCounts extends strin
|
|
|
106
109
|
withTrashed(): this;
|
|
107
110
|
onlyTrashed(): this;
|
|
108
111
|
get(): Promise<ModelQueryResult<TModel, TCounts>[]>;
|
|
112
|
+
keysetPaginate(options: KeysetOptions<ModelAttributes<TModel>>): Promise<{
|
|
113
|
+
meta: import("../pagination/index.ts").CursorPaginationMeta<KeysetCursor>;
|
|
114
|
+
data: ModelQueryResult<TModel, TCounts>[];
|
|
115
|
+
}>;
|
|
109
116
|
private hydrateRows;
|
|
110
117
|
first(): Promise<ModelQueryResult<TModel, TCounts> | null>;
|
|
111
118
|
count(): Promise<number>;
|
|
@@ -186,6 +193,12 @@ declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & s
|
|
|
186
193
|
static chunk<TModel extends object>(this: {
|
|
187
194
|
prototype: TModel;
|
|
188
195
|
}, count: number, callback: (models: TModel[]) => Promise<boolean | void>): Promise<void>;
|
|
196
|
+
static keysetPaginate<TModel extends object>(this: {
|
|
197
|
+
prototype: TModel;
|
|
198
|
+
}, options: KeysetOptions<ModelAttributes<TModel>>): Promise<{
|
|
199
|
+
meta: import("../pagination/index.ts").CursorPaginationMeta<KeysetCursor>;
|
|
200
|
+
data: TModel[];
|
|
201
|
+
}>;
|
|
189
202
|
static cursorPaginate<TModel extends object>(this: {
|
|
190
203
|
prototype: TModel;
|
|
191
204
|
}, options: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PaginatedResult } from "../pagination/index.ts";
|
|
1
|
+
import type { KeysetOptions, PaginatedResult } from "../pagination/index.ts";
|
|
2
2
|
import type BaseRepository from "./baseRepository.ts";
|
|
3
3
|
import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
|
|
4
4
|
import type { QueryLockOptions, QueryOptions, QueryWhere, UpdateValues } from "./types.ts";
|
|
@@ -66,6 +66,14 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
|
|
|
66
66
|
meta: import("../pagination/index.ts").CursorPaginationMeta<TEntity[PrimaryKey]>;
|
|
67
67
|
data: (TEntity & LoadedRow)[];
|
|
68
68
|
}>;
|
|
69
|
+
keysetPaginate(options: KeysetOptions<TEntity>): Promise<{
|
|
70
|
+
meta: import("../pagination/index.ts").CursorPaginationMeta<import("../pagination/index.ts").KeysetCursor>;
|
|
71
|
+
data: (TEntity & LoadedRow)[];
|
|
72
|
+
}>;
|
|
73
|
+
projectKeyset<K extends keyof TEntity & string>(columns: readonly K[], options: KeysetOptions<TEntity>): Promise<{
|
|
74
|
+
meta: import("../pagination/index.ts").CursorPaginationMeta<import("../pagination/index.ts").KeysetCursor>;
|
|
75
|
+
data: Array<Pick<TEntity, K>>;
|
|
76
|
+
}>;
|
|
69
77
|
/** Compiled query filters for batched relationship loaders. */
|
|
70
78
|
getOptions(): QueryOptions<TEntity>;
|
|
71
79
|
private buildOptions;
|
|
@@ -13,6 +13,13 @@ type WhereNode<TEntity extends object> = {
|
|
|
13
13
|
} | {
|
|
14
14
|
kind: "and" | "or";
|
|
15
15
|
exists: ExistsClause;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "and" | "or";
|
|
18
|
+
compareRow: {
|
|
19
|
+
columns: readonly string[];
|
|
20
|
+
operator: "lt" | "gt";
|
|
21
|
+
values: readonly unknown[];
|
|
22
|
+
};
|
|
16
23
|
};
|
|
17
24
|
declare class WhereBuilder<TEntity extends object, TWhere extends object = QueryWhere<TEntity>> {
|
|
18
25
|
readonly nodes: WhereNode<TEntity>[];
|
|
@@ -25,3 +25,24 @@ declare function buildPaginationMeta(input: {
|
|
|
25
25
|
}): PaginationMeta;
|
|
26
26
|
export type { CursorPaginatedResult, CursorPaginationMeta, PaginatedResult, PaginationMeta };
|
|
27
27
|
export { buildPaginationMeta };
|
|
28
|
+
/** Ordering columns must be non-null scalar fields in the underlying schema. */
|
|
29
|
+
type KeysetColumn<TEntity> = {
|
|
30
|
+
[K in keyof TEntity & string]: TEntity[K] extends string | number | bigint | Date ? K : never;
|
|
31
|
+
}[keyof TEntity & string];
|
|
32
|
+
interface KeysetCursor {
|
|
33
|
+
version: 1;
|
|
34
|
+
order: Array<{
|
|
35
|
+
column: string;
|
|
36
|
+
direction: "asc" | "desc";
|
|
37
|
+
}>;
|
|
38
|
+
values: string[];
|
|
39
|
+
}
|
|
40
|
+
interface KeysetOptions<TEntity> {
|
|
41
|
+
perPage: number;
|
|
42
|
+
orderBy: readonly {
|
|
43
|
+
column: KeysetColumn<TEntity>;
|
|
44
|
+
direction: "asc" | "desc";
|
|
45
|
+
}[];
|
|
46
|
+
cursor?: KeysetCursor;
|
|
47
|
+
}
|
|
48
|
+
export type { KeysetColumn, KeysetCursor, KeysetOptions };
|
|
@@ -149,6 +149,12 @@ function remapExistsSql(sql, existsParams, params) {
|
|
|
149
149
|
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
150
150
|
}
|
|
151
151
|
function buildWhereNodeClause(tableName, node, params) {
|
|
152
|
+
if ("compareRow" in node) {
|
|
153
|
+
const { columns, operator, values } = node.compareRow;
|
|
154
|
+
if (!Array.isArray(columns) || !columns.length || !Array.isArray(values) || columns.length !== values.length || !["lt", "gt"].includes(operator) || Array.from(columns).some((column) => typeof column !== "string" || !column.length))
|
|
155
|
+
throw new TypeError("Invalid row comparison columns, values or operator.");
|
|
156
|
+
return `(${columns.map((column) => qualifyColumn(tableName, column)).join(", ")}) ${operator === "lt" ? "<" : ">"} (${values.map((value) => pushParam(params, value)).join(", ")})`;
|
|
157
|
+
}
|
|
152
158
|
if ("exists" in node) {
|
|
153
159
|
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
154
160
|
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
@@ -305,6 +311,12 @@ function buildSelectList(table, select, params = []) {
|
|
|
305
311
|
const column = qualifyColumn(item.table, item.column);
|
|
306
312
|
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
307
313
|
}
|
|
314
|
+
if (item.kind === "textColumn") {
|
|
315
|
+
const dialect = currentSqlDialect();
|
|
316
|
+
const column = qualifyColumn(item.table, item.column);
|
|
317
|
+
const text = dialect.driver === "sqlite" ? `CASE WHEN typeof(${column}) = 'real' THEN printf('%!.26g', ${column}) ELSE ${dialect.castToText(column)} END` : dialect.castToText(column);
|
|
318
|
+
return `${text} AS ${quoteIdentifier(item.as)}`;
|
|
319
|
+
}
|
|
308
320
|
if (item.kind === "literalText") {
|
|
309
321
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
310
322
|
}
|
|
@@ -173,6 +173,12 @@ function remapExistsSql(sql, existsParams, params) {
|
|
|
173
173
|
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
174
174
|
}
|
|
175
175
|
function buildWhereNodeClause(tableName, node, params) {
|
|
176
|
+
if ("compareRow" in node) {
|
|
177
|
+
const { columns, operator, values } = node.compareRow;
|
|
178
|
+
if (!Array.isArray(columns) || !columns.length || !Array.isArray(values) || columns.length !== values.length || !["lt", "gt"].includes(operator) || Array.from(columns).some((column) => typeof column !== "string" || !column.length))
|
|
179
|
+
throw new TypeError("Invalid row comparison columns, values or operator.");
|
|
180
|
+
return `(${columns.map((column) => qualifyColumn(tableName, column)).join(", ")}) ${operator === "lt" ? "<" : ">"} (${values.map((value) => pushParam(params, value)).join(", ")})`;
|
|
181
|
+
}
|
|
176
182
|
if ("exists" in node) {
|
|
177
183
|
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
178
184
|
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
@@ -329,6 +335,12 @@ function buildSelectList(table, select, params = []) {
|
|
|
329
335
|
const column = qualifyColumn(item.table, item.column);
|
|
330
336
|
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
331
337
|
}
|
|
338
|
+
if (item.kind === "textColumn") {
|
|
339
|
+
const dialect = currentSqlDialect();
|
|
340
|
+
const column = qualifyColumn(item.table, item.column);
|
|
341
|
+
const text = dialect.driver === "sqlite" ? `CASE WHEN typeof(${column}) = 'real' THEN printf('%!.26g', ${column}) ELSE ${dialect.castToText(column)} END` : dialect.castToText(column);
|
|
342
|
+
return `${text} AS ${quoteIdentifier(item.as)}`;
|
|
343
|
+
}
|
|
332
344
|
if (item.kind === "literalText") {
|
|
333
345
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
334
346
|
}
|
|
@@ -1157,6 +1169,20 @@ class RepositoryQuery {
|
|
|
1157
1169
|
const page = await this.repository.cursorPaginate({ ...this.buildOptions(), ...options });
|
|
1158
1170
|
return { ...page, data: await this.attach(page.data) };
|
|
1159
1171
|
}
|
|
1172
|
+
async keysetPaginate(options) {
|
|
1173
|
+
const page = await this.repository.keysetPaginate({ ...this.buildOptions(), ...options });
|
|
1174
|
+
return { ...page, data: await this.attach(page.data) };
|
|
1175
|
+
}
|
|
1176
|
+
async projectKeyset(columns, options) {
|
|
1177
|
+
if (!columns.length || columns.some((column) => !this.repository.getTable().columns.includes(column)))
|
|
1178
|
+
throw new TypeError("Keyset projections require declared columns.");
|
|
1179
|
+
const page = await this.repository.keysetPaginate({
|
|
1180
|
+
...this.buildOptions(),
|
|
1181
|
+
...options,
|
|
1182
|
+
select: uniqueColumnSelect(this.repository.getTable().name, columns)
|
|
1183
|
+
});
|
|
1184
|
+
return { ...page, data: page.data };
|
|
1185
|
+
}
|
|
1160
1186
|
getOptions() {
|
|
1161
1187
|
return this.buildOptions();
|
|
1162
1188
|
}
|
|
@@ -126,7 +126,7 @@ export type { MetricLabels } from "../core/metrics/prometheus.ts";
|
|
|
126
126
|
export { PrometheusRegistry, prometheusRegistry } from "../core/metrics/prometheus.ts";
|
|
127
127
|
export type { DatabaseNotificationPayload, DatabaseNotificationStore, MailNotificationMessage, Notifiable, NotificationChannelName, } from "../core/notifications/index.ts";
|
|
128
128
|
export { createNotificationDispatcher, Notification, NotificationDispatcher, } from "../core/notifications/index.ts";
|
|
129
|
-
export type { CursorPaginatedResult, PaginatedResult, PaginationMeta, } from "../core/pagination/index.ts";
|
|
129
|
+
export type { CursorPaginatedResult, KeysetColumn, KeysetCursor, KeysetOptions, PaginatedResult, PaginationMeta, } from "../core/pagination/index.ts";
|
|
130
130
|
export type { Queue, QueuePriority } from "../core/queue/index.ts";
|
|
131
131
|
export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts";
|
|
132
132
|
export { JobRegistry } from "../core/queue/jobRegistry.ts";
|
package/dist/index.js
CHANGED
|
@@ -4131,6 +4131,58 @@ function buildPaginationMeta(input) {
|
|
|
4131
4131
|
init_connectionContext();
|
|
4132
4132
|
init_dialect();
|
|
4133
4133
|
|
|
4134
|
+
// ../../src/core/database/keyset.ts
|
|
4135
|
+
init_dialect();
|
|
4136
|
+
var KEYSET_ALIAS_PREFIX = "__strata_keyset_";
|
|
4137
|
+
function validateKeyset(table, options) {
|
|
4138
|
+
if (!Number.isInteger(options.perPage) || options.perPage < 1 || options.perPage > 1000)
|
|
4139
|
+
throw new RangeError("Keyset page size must be between 1 and 1000.");
|
|
4140
|
+
if (!Array.isArray(options.orderBy) || options.orderBy.length < 1 || options.orderBy.length > 8)
|
|
4141
|
+
throw new TypeError("Keyset ordering requires between 1 and 8 columns.");
|
|
4142
|
+
if (table.columns.some((column) => column.toLowerCase().startsWith(KEYSET_ALIAS_PREFIX)))
|
|
4143
|
+
throw new TypeError("Table columns conflict with reserved keyset aliases.");
|
|
4144
|
+
const seen = new Set;
|
|
4145
|
+
const order = Array.from(options.orderBy).map(({ column, direction }) => {
|
|
4146
|
+
if (!table.columns.includes(column) || seen.has(column) || !["asc", "desc"].includes(direction))
|
|
4147
|
+
throw new TypeError("Invalid or duplicate keyset ordering column/direction.");
|
|
4148
|
+
seen.add(column);
|
|
4149
|
+
return { column, direction };
|
|
4150
|
+
});
|
|
4151
|
+
if (order.at(-1)?.column !== table.primaryKey)
|
|
4152
|
+
throw new TypeError("Keyset ordering must end with the table primary key.");
|
|
4153
|
+
if (options.cursor !== undefined) {
|
|
4154
|
+
const cursor = options.cursor;
|
|
4155
|
+
if (cursor?.version !== 1 || !Array.isArray(cursor.order) || cursor.order.length !== order.length || !Array.isArray(cursor.values) || cursor.values.length !== order.length || Array.from(cursor.order).some((item, i) => !item || item.column !== order[i]?.column || item.direction !== order[i]?.direction) || Array.from(cursor.values).some((value) => typeof value !== "string" || value.length > 4096))
|
|
4156
|
+
throw new TypeError("Invalid keyset cursor or ordering mismatch.");
|
|
4157
|
+
}
|
|
4158
|
+
return order;
|
|
4159
|
+
}
|
|
4160
|
+
function keysetBoundary(order, cursor) {
|
|
4161
|
+
if (!cursor)
|
|
4162
|
+
return [];
|
|
4163
|
+
if (currentSqlDialect().driver === "pgsql" && order.length > 1 && order.every((item) => item.direction === order[0]?.direction)) {
|
|
4164
|
+
return [
|
|
4165
|
+
{
|
|
4166
|
+
kind: "and",
|
|
4167
|
+
compareRow: {
|
|
4168
|
+
columns: order.map((item) => item.column),
|
|
4169
|
+
operator: order[0]?.direction === "asc" ? "gt" : "lt",
|
|
4170
|
+
values: [...cursor.values]
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
];
|
|
4174
|
+
}
|
|
4175
|
+
const branches = order.map((item, i) => {
|
|
4176
|
+
const where = {};
|
|
4177
|
+
order.slice(0, i).forEach((prefix, j) => {
|
|
4178
|
+
where[prefix.column] = cursor.values[j];
|
|
4179
|
+
});
|
|
4180
|
+
where[item.column] = { [item.direction === "asc" ? "gt" : "lt"]: cursor.values[i] };
|
|
4181
|
+
return { kind: i === 0 ? "and" : "or", where };
|
|
4182
|
+
});
|
|
4183
|
+
return [{ kind: "and", group: branches }];
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4134
4186
|
// ../../src/core/database/query.ts
|
|
4135
4187
|
init_dialect();
|
|
4136
4188
|
function quoteIdentifier(identifier) {
|
|
@@ -4281,6 +4333,12 @@ function remapExistsSql(sql, existsParams, params) {
|
|
|
4281
4333
|
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
4282
4334
|
}
|
|
4283
4335
|
function buildWhereNodeClause(tableName, node, params) {
|
|
4336
|
+
if ("compareRow" in node) {
|
|
4337
|
+
const { columns, operator, values } = node.compareRow;
|
|
4338
|
+
if (!Array.isArray(columns) || !columns.length || !Array.isArray(values) || columns.length !== values.length || !["lt", "gt"].includes(operator) || Array.from(columns).some((column) => typeof column !== "string" || !column.length))
|
|
4339
|
+
throw new TypeError("Invalid row comparison columns, values or operator.");
|
|
4340
|
+
return `(${columns.map((column) => qualifyColumn(tableName, column)).join(", ")}) ${operator === "lt" ? "<" : ">"} (${values.map((value) => pushParam(params, value)).join(", ")})`;
|
|
4341
|
+
}
|
|
4284
4342
|
if ("exists" in node) {
|
|
4285
4343
|
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
4286
4344
|
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
@@ -4437,6 +4495,12 @@ function buildSelectList(table, select, params = []) {
|
|
|
4437
4495
|
const column = qualifyColumn(item.table, item.column);
|
|
4438
4496
|
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
4439
4497
|
}
|
|
4498
|
+
if (item.kind === "textColumn") {
|
|
4499
|
+
const dialect = currentSqlDialect();
|
|
4500
|
+
const column = qualifyColumn(item.table, item.column);
|
|
4501
|
+
const text = dialect.driver === "sqlite" ? `CASE WHEN typeof(${column}) = 'real' THEN printf('%!.26g', ${column}) ELSE ${dialect.castToText(column)} END` : dialect.castToText(column);
|
|
4502
|
+
return `${text} AS ${quoteIdentifier(item.as)}`;
|
|
4503
|
+
}
|
|
4440
4504
|
if (item.kind === "literalText") {
|
|
4441
4505
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
4442
4506
|
}
|
|
@@ -5292,6 +5356,20 @@ class RepositoryQuery {
|
|
|
5292
5356
|
const page = await this.repository.cursorPaginate({ ...this.buildOptions(), ...options });
|
|
5293
5357
|
return { ...page, data: await this.attach(page.data) };
|
|
5294
5358
|
}
|
|
5359
|
+
async keysetPaginate(options) {
|
|
5360
|
+
const page = await this.repository.keysetPaginate({ ...this.buildOptions(), ...options });
|
|
5361
|
+
return { ...page, data: await this.attach(page.data) };
|
|
5362
|
+
}
|
|
5363
|
+
async projectKeyset(columns, options) {
|
|
5364
|
+
if (!columns.length || columns.some((column) => !this.repository.getTable().columns.includes(column)))
|
|
5365
|
+
throw new TypeError("Keyset projections require declared columns.");
|
|
5366
|
+
const page = await this.repository.keysetPaginate({
|
|
5367
|
+
...this.buildOptions(),
|
|
5368
|
+
...options,
|
|
5369
|
+
select: uniqueColumnSelect(this.repository.getTable().name, columns)
|
|
5370
|
+
});
|
|
5371
|
+
return { ...page, data: page.data };
|
|
5372
|
+
}
|
|
5295
5373
|
getOptions() {
|
|
5296
5374
|
return this.buildOptions();
|
|
5297
5375
|
}
|
|
@@ -5557,6 +5635,65 @@ class BaseRepository {
|
|
|
5557
5635
|
}
|
|
5558
5636
|
};
|
|
5559
5637
|
}
|
|
5638
|
+
async keysetPaginate(options) {
|
|
5639
|
+
const order = validateKeyset(this.table, options);
|
|
5640
|
+
if (options.joins?.length || options.groupBy || options.having || "offset" in options || "limit" in options)
|
|
5641
|
+
throw new TypeError("Keyset pagination does not accept joined/grouped/offset/limited queries.");
|
|
5642
|
+
if (options.select?.some((item) => item.as?.toLowerCase().startsWith(KEYSET_ALIAS_PREFIX)))
|
|
5643
|
+
throw new TypeError("Projection conflicts with reserved keyset aliases.");
|
|
5644
|
+
const filters = [...options.whereNodes ?? []];
|
|
5645
|
+
if (options.where)
|
|
5646
|
+
filters.push({ kind: "and", where: options.where });
|
|
5647
|
+
const rows = await this.findAll({
|
|
5648
|
+
...options,
|
|
5649
|
+
where: {},
|
|
5650
|
+
whereNodes: [
|
|
5651
|
+
{ kind: "and", group: filters },
|
|
5652
|
+
...keysetBoundary(order, options.cursor)
|
|
5653
|
+
],
|
|
5654
|
+
orderBy: order,
|
|
5655
|
+
limit: options.perPage + 1,
|
|
5656
|
+
select: [
|
|
5657
|
+
...options.select?.length ? options.select : this.table.columns.map((column) => ({
|
|
5658
|
+
kind: "column",
|
|
5659
|
+
table: this.table.name,
|
|
5660
|
+
column
|
|
5661
|
+
})),
|
|
5662
|
+
...order.map((item, i) => ({
|
|
5663
|
+
kind: "textColumn",
|
|
5664
|
+
table: this.table.name,
|
|
5665
|
+
column: item.column,
|
|
5666
|
+
as: `${KEYSET_ALIAS_PREFIX}${i}`
|
|
5667
|
+
}))
|
|
5668
|
+
]
|
|
5669
|
+
});
|
|
5670
|
+
const entries = rows.map((row) => {
|
|
5671
|
+
const record = { ...row };
|
|
5672
|
+
const values = order.map((_, i) => {
|
|
5673
|
+
const key = `${KEYSET_ALIAS_PREFIX}${i}`;
|
|
5674
|
+
const value = record[key];
|
|
5675
|
+
delete record[key];
|
|
5676
|
+
if (typeof value !== "string" || value.length > 4096)
|
|
5677
|
+
throw new TypeError("Keyset columns must contain non-null bounded scalar values.");
|
|
5678
|
+
return value;
|
|
5679
|
+
});
|
|
5680
|
+
return { record, values };
|
|
5681
|
+
});
|
|
5682
|
+
const hasMore = entries.length > options.perPage;
|
|
5683
|
+
const visible = entries.slice(0, options.perPage);
|
|
5684
|
+
const last = visible.at(-1);
|
|
5685
|
+
const nextCursor = hasMore && last ? { version: 1, order, values: last.values } : null;
|
|
5686
|
+
const data = visible.map(({ record }) => record);
|
|
5687
|
+
return {
|
|
5688
|
+
data,
|
|
5689
|
+
meta: {
|
|
5690
|
+
per_page: options.perPage,
|
|
5691
|
+
has_more: hasMore,
|
|
5692
|
+
next_cursor: nextCursor,
|
|
5693
|
+
prev_cursor: options.cursor ?? null
|
|
5694
|
+
}
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5560
5697
|
async findById(id) {
|
|
5561
5698
|
return await this.firstOrNull({
|
|
5562
5699
|
[this.table.primaryKey]: id
|
|
@@ -7244,8 +7381,13 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
|
7244
7381
|
|
|
7245
7382
|
class ModelProjection {
|
|
7246
7383
|
load;
|
|
7247
|
-
|
|
7384
|
+
page;
|
|
7385
|
+
constructor(load, page) {
|
|
7248
7386
|
this.load = load;
|
|
7387
|
+
this.page = page;
|
|
7388
|
+
}
|
|
7389
|
+
keysetPaginate(options) {
|
|
7390
|
+
return this.page(options);
|
|
7249
7391
|
}
|
|
7250
7392
|
get() {
|
|
7251
7393
|
return this.load();
|
|
@@ -7295,6 +7437,12 @@ class ModelQuery {
|
|
|
7295
7437
|
return new ModelProjection(async (limit) => {
|
|
7296
7438
|
const rows = await this.query.project(columns, limit);
|
|
7297
7439
|
return rows.map((row) => applyCasts(row, modelStatics(this.modelClass).$casts, "hydrate"));
|
|
7440
|
+
}, async (options) => {
|
|
7441
|
+
const page = await this.query.projectKeyset(columns, options);
|
|
7442
|
+
return {
|
|
7443
|
+
...page,
|
|
7444
|
+
data: page.data.map((row) => applyCasts(row, modelStatics(this.modelClass).$casts, "hydrate"))
|
|
7445
|
+
};
|
|
7298
7446
|
});
|
|
7299
7447
|
}
|
|
7300
7448
|
lockForUpdate(options = {}) {
|
|
@@ -7412,6 +7560,10 @@ class ModelQuery {
|
|
|
7412
7560
|
async get() {
|
|
7413
7561
|
return await this.hydrateRows(await this.query.get());
|
|
7414
7562
|
}
|
|
7563
|
+
async keysetPaginate(options) {
|
|
7564
|
+
const page = await this.query.keysetPaginate(options);
|
|
7565
|
+
return { ...page, data: await this.hydrateRows(page.data) };
|
|
7566
|
+
}
|
|
7415
7567
|
async hydrateRows(rows) {
|
|
7416
7568
|
const statics = modelStatics(this.modelClass);
|
|
7417
7569
|
const models = [];
|
|
@@ -7695,6 +7847,9 @@ class Model {
|
|
|
7695
7847
|
return await callback(rows.map((row) => statics.newFromRecord(row, true)));
|
|
7696
7848
|
});
|
|
7697
7849
|
}
|
|
7850
|
+
static keysetPaginate(options) {
|
|
7851
|
+
return Model.query.call(this).keysetPaginate(options);
|
|
7852
|
+
}
|
|
7698
7853
|
static async cursorPaginate(options) {
|
|
7699
7854
|
const statics = modelStatics(this);
|
|
7700
7855
|
ensureBooted(this);
|