@platformatic/sql-mapper 2.55.0 → 2.57.0
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/lib/cursor.js +86 -0
- package/lib/entity.js +25 -8
- package/lib/errors.js +3 -0
- package/mapper.d.ts +18 -0
- package/package.json +3 -3
package/lib/cursor.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const errors = require('./errors')
|
|
4
|
+
|
|
5
|
+
function sanitizeCursor (cursor, orderBy, inputToFieldMap, fields, primaryKeys) {
|
|
6
|
+
if (!orderBy || orderBy.length === 0) throw new errors.MissingOrderByClauseError()
|
|
7
|
+
let hasUniqueField = false
|
|
8
|
+
const validCursorFields = new Map()
|
|
9
|
+
|
|
10
|
+
for (const [key, value] of Object.entries(cursor)) {
|
|
11
|
+
const dbField = inputToFieldMap[key]
|
|
12
|
+
if (!dbField) throw new errors.UnknownFieldError(key)
|
|
13
|
+
const order = orderBy.find((order) => order.field === key)
|
|
14
|
+
if (!order) throw new errors.MissingOrderByFieldForCursorError(key)
|
|
15
|
+
if (primaryKeys.has(dbField)) hasUniqueField = true
|
|
16
|
+
validCursorFields.set(key, {
|
|
17
|
+
dbField,
|
|
18
|
+
value,
|
|
19
|
+
direction: order.direction.toLowerCase(),
|
|
20
|
+
fieldWrap: fields[dbField]
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
if (!hasUniqueField) throw new errors.MissingUniqueFieldInCursorError()
|
|
24
|
+
|
|
25
|
+
// Process fields in orderBy order
|
|
26
|
+
const cursorFields = []
|
|
27
|
+
for (const order of orderBy) {
|
|
28
|
+
if (validCursorFields.has(order.field)) {
|
|
29
|
+
cursorFields.push(validCursorFields.get(order.field))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return cursorFields
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildTupleQuery (cursorFields, sql, computeCriteriaValue, isBackwardPagination) {
|
|
36
|
+
const direction = cursorFields[0].direction
|
|
37
|
+
let operator
|
|
38
|
+
if (isBackwardPagination) {
|
|
39
|
+
operator = direction === 'desc' ? '>' : '<'
|
|
40
|
+
} else {
|
|
41
|
+
operator = direction === 'desc' ? '<' : '>'
|
|
42
|
+
}
|
|
43
|
+
const fields = sql.join(
|
|
44
|
+
cursorFields.map(({ dbField }) => sql.ident(dbField)),
|
|
45
|
+
sql`, `
|
|
46
|
+
)
|
|
47
|
+
const values = sql.join(
|
|
48
|
+
cursorFields.map(({ fieldWrap, value }) => computeCriteriaValue(fieldWrap, value)),
|
|
49
|
+
sql`, `
|
|
50
|
+
)
|
|
51
|
+
return sql`(${fields}) ${sql.__dangerous__rawValue(operator)} (${values})`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildQuery (cursorFields, sql, computeCriteriaValue, isBackwardPagination) {
|
|
55
|
+
const conditions = []
|
|
56
|
+
const equalityParts = []
|
|
57
|
+
for (const { dbField, fieldWrap, value, direction } of cursorFields) {
|
|
58
|
+
let operator
|
|
59
|
+
if (isBackwardPagination) {
|
|
60
|
+
operator = direction === 'desc' ? '>' : '<'
|
|
61
|
+
} else {
|
|
62
|
+
operator = direction === 'desc' ? '<' : '>'
|
|
63
|
+
}
|
|
64
|
+
const inequalityPart = sql`${sql.ident(dbField)} ${sql.__dangerous__rawValue(operator)} ${computeCriteriaValue(fieldWrap, value)}`
|
|
65
|
+
if (equalityParts.length === 0) {
|
|
66
|
+
conditions.push(inequalityPart)
|
|
67
|
+
} else {
|
|
68
|
+
conditions.push(sql`${sql.join(equalityParts, sql` AND `)} AND ${inequalityPart}`)
|
|
69
|
+
}
|
|
70
|
+
equalityParts.push(sql`${sql.ident(dbField)} = ${computeCriteriaValue(fieldWrap, value)}`)
|
|
71
|
+
}
|
|
72
|
+
return sql`(${sql.join(conditions, sql` OR `)})`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildCursorCondition (sql, cursor, orderBy, inputToFieldMap, fields, computeCriteriaValue, primaryKeys, isBackwardPagination) {
|
|
76
|
+
if (!cursor || Object.keys(cursor).length === 0) return null
|
|
77
|
+
const cursorFields = sanitizeCursor(cursor, orderBy, inputToFieldMap, fields, primaryKeys)
|
|
78
|
+
const sameSortDirection = cursorFields.every(({ direction }) => direction === cursorFields[0].direction)
|
|
79
|
+
return sameSortDirection
|
|
80
|
+
? buildTupleQuery(cursorFields, sql, computeCriteriaValue, isBackwardPagination)
|
|
81
|
+
: buildQuery(cursorFields, sql, computeCriteriaValue, isBackwardPagination)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
buildCursorCondition,
|
|
86
|
+
}
|
package/lib/entity.js
CHANGED
|
@@ -12,6 +12,7 @@ const { singularize } = require('inflected')
|
|
|
12
12
|
const { findNearestString } = require('@platformatic/utils')
|
|
13
13
|
const errors = require('./errors')
|
|
14
14
|
const { wrapDB } = require('./telemetry')
|
|
15
|
+
const { buildCursorCondition } = require('./cursor')
|
|
15
16
|
|
|
16
17
|
function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relations, queries, autoTimestamp, schema, useSchemaInName, limitConfig, columns, constraintsList) {
|
|
17
18
|
/* istanbul ignore next */ // Ignoring because this won't be fully covered by DB not supporting schemas (SQLite)
|
|
@@ -306,35 +307,51 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
|
|
|
306
307
|
const db = getDB(opts)
|
|
307
308
|
const fieldsToRetrieve = computeFields(opts.fields).map((f) => sql.ident(f))
|
|
308
309
|
const criteria = computeCriteria(opts)
|
|
310
|
+
const criteriaExists = criteria.length > 0
|
|
311
|
+
const isBackwardPagination = opts.nextPage === false
|
|
309
312
|
|
|
310
313
|
let query = sql`
|
|
311
314
|
SELECT ${sql.join(fieldsToRetrieve, sql`, `)}
|
|
312
315
|
FROM ${tableName(sql, table, schema)}
|
|
313
316
|
`
|
|
314
317
|
|
|
315
|
-
if (
|
|
318
|
+
if (criteriaExists) {
|
|
316
319
|
query = sql`${query} WHERE ${sql.join(criteria, sql` AND `)}`
|
|
317
320
|
}
|
|
318
321
|
|
|
322
|
+
if (opts.cursor) {
|
|
323
|
+
const cursorCondition = buildCursorCondition(sql, opts.cursor, opts.orderBy, inputToFieldMap, fields, computeCriteriaValue, primaryKeys, isBackwardPagination)
|
|
324
|
+
if (cursorCondition) {
|
|
325
|
+
if (criteriaExists) query = sql`${query} AND ${cursorCondition}`
|
|
326
|
+
else query = sql`${query} WHERE ${cursorCondition}`
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
319
330
|
if (opts.orderBy && opts.orderBy.length > 0) {
|
|
320
331
|
const orderBy = opts.orderBy.map((order) => {
|
|
321
332
|
const field = inputToFieldMap[order.field]
|
|
322
|
-
|
|
333
|
+
let direction = order.direction.toLowerCase()
|
|
334
|
+
if (isBackwardPagination) {
|
|
335
|
+
direction = direction === 'asc' ? 'desc' : 'asc'
|
|
336
|
+
}
|
|
337
|
+
return sql`${sql.ident(field)} ${sql.__dangerous__rawValue(direction)}`
|
|
323
338
|
})
|
|
324
339
|
query = sql`${query} ORDER BY ${sql.join(orderBy, sql`, `)}`
|
|
325
340
|
}
|
|
326
341
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (opts.offset
|
|
330
|
-
|
|
342
|
+
if (opts.paginate !== false) {
|
|
343
|
+
query = sql`${query} LIMIT ${sanitizeLimit(opts.limit, limitConfig)}`
|
|
344
|
+
if (opts.offset !== undefined) {
|
|
345
|
+
if (opts.offset < 0) {
|
|
346
|
+
throw new errors.ParamNotAllowedError(opts.offset)
|
|
347
|
+
}
|
|
348
|
+
query = sql`${query} OFFSET ${opts.offset}`
|
|
331
349
|
}
|
|
332
|
-
query = sql`${query} OFFSET ${opts.offset}`
|
|
333
350
|
}
|
|
334
351
|
|
|
335
352
|
const rows = await db.query(query)
|
|
336
353
|
const res = rows.map(fixOutput)
|
|
337
|
-
return res
|
|
354
|
+
return isBackwardPagination ? res.reverse() : res
|
|
338
355
|
}
|
|
339
356
|
|
|
340
357
|
async function count (opts = {}) {
|
package/lib/errors.js
CHANGED
|
@@ -21,4 +21,7 @@ module.exports = {
|
|
|
21
21
|
MissingValueForPrimaryKeyError: createError(`${ERROR_PREFIX}_MISSING_VALUE_FOR_PRIMARY_KEY`, 'Missing value for primary key %s'),
|
|
22
22
|
MissingWhereClauseError: createError(`${ERROR_PREFIX}_MISSING_WHERE_CLAUSE`, 'Missing where clause', 400),
|
|
23
23
|
SQLiteOnlySupportsAutoIncrementOnOneColumnError: createError(`${ERROR_PREFIX}_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN`, 'SQLite only supports autoIncrement on one column'),
|
|
24
|
+
MissingOrderByClauseError: createError(`${ERROR_PREFIX}_MISSING_ORDER_BY_CLAUSE`, 'Missing orderBy clause'),
|
|
25
|
+
MissingOrderByFieldForCursorError: createError(`${ERROR_PREFIX}_MISSING_ORDER_BY_FIELD_FOR_CURSOR`, 'Cursor field(s) %s must be included in orderBy'),
|
|
26
|
+
MissingUniqueFieldInCursorError: createError(`${ERROR_PREFIX}_MISSING_UNIQUE_FIELD_IN_CURSOR`, 'Cursor must contain at least one primary key field'),
|
|
24
27
|
}
|
package/mapper.d.ts
CHANGED
|
@@ -140,6 +140,10 @@ export interface WhereCondition {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export type Cursor = {
|
|
144
|
+
[columnName: string]: string | number | boolean | null,
|
|
145
|
+
}
|
|
146
|
+
|
|
143
147
|
interface Find<EntityFields> {
|
|
144
148
|
(options?: {
|
|
145
149
|
/**
|
|
@@ -162,6 +166,20 @@ interface Find<EntityFields> {
|
|
|
162
166
|
* Number of entities to skip.
|
|
163
167
|
*/
|
|
164
168
|
offset?: number,
|
|
169
|
+
/**
|
|
170
|
+
* If false pagination is disabled.
|
|
171
|
+
* @default true
|
|
172
|
+
*/
|
|
173
|
+
paginate?: boolean,
|
|
174
|
+
/**
|
|
175
|
+
* Cursor to paginate the results.
|
|
176
|
+
*/
|
|
177
|
+
cursor?: Cursor,
|
|
178
|
+
/**
|
|
179
|
+
* If set to false, the previous page will be fetched in cursor pagination.
|
|
180
|
+
* @default true
|
|
181
|
+
*/
|
|
182
|
+
nextPage?: boolean,
|
|
165
183
|
/**
|
|
166
184
|
* If present, the entity participates in transaction
|
|
167
185
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/sql-mapper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.57.0",
|
|
4
4
|
"description": "A data mapper utility for SQL databases",
|
|
5
5
|
"main": "mapper.js",
|
|
6
6
|
"types": "mapper.d.ts",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"camelcase": "^6.3.0",
|
|
36
36
|
"fastify-plugin": "^5.0.0",
|
|
37
37
|
"inflected": "^2.1.0",
|
|
38
|
-
"@platformatic/
|
|
39
|
-
"@platformatic/
|
|
38
|
+
"@platformatic/utils": "2.57.0",
|
|
39
|
+
"@platformatic/telemetry": "2.57.0"
|
|
40
40
|
},
|
|
41
41
|
"tsd": {
|
|
42
42
|
"directory": "test/types"
|