@platformatic/sql-mapper 2.56.0 → 2.58.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 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,20 +307,34 @@ 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 (criteria.length > 0) {
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
- return sql`${sql.ident(field)} ${sql.__dangerous__rawValue(order.direction)}`
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
  }
@@ -336,7 +351,7 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
336
351
 
337
352
  const rows = await db.query(query)
338
353
  const res = rows.map(fixOutput)
339
- return res
354
+ return isBackwardPagination ? res.reverse() : res
340
355
  }
341
356
 
342
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
  /**
@@ -167,6 +171,15 @@ interface Find<EntityFields> {
167
171
  * @default true
168
172
  */
169
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,
170
183
  /**
171
184
  * If present, the entity participates in transaction
172
185
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/sql-mapper",
3
- "version": "2.56.0",
3
+ "version": "2.58.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/telemetry": "2.56.0",
39
- "@platformatic/utils": "2.56.0"
38
+ "@platformatic/telemetry": "2.58.0",
39
+ "@platformatic/utils": "2.58.0"
40
40
  },
41
41
  "tsd": {
42
42
  "directory": "test/types"