@restura/core 1.2.0 → 1.4.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/dist/index.d.ts CHANGED
@@ -1070,6 +1070,20 @@ declare abstract class SqlEngine {
1070
1070
  protected abstract generateOrderBy(req: RsRequest<unknown>, routeData: StandardRouteData): string;
1071
1071
  protected abstract generateWhereClause(req: RsRequest<unknown>, where: WhereData[], routeData: StandardRouteData, sqlParams: string[]): string;
1072
1072
  protected replaceParamKeywords(value: string | number, routeData: RouteData, req: RsRequest<unknown>, sqlParams: string[]): string | number;
1073
+ /**
1074
+ * Replaces local parameter keywords (e.g., $paramName) in SQL query strings with '?' placeholders
1075
+ * and adds the corresponding parameter values to the sqlParams array for parameterized queries.
1076
+ *
1077
+ * Validates that each parameter keyword exists in the route's request schema before processing.
1078
+ * If the value is not a string or routeData has no request schema, returns the value unchanged.
1079
+ *
1080
+ * @param value - The string or number value that may contain parameter keywords (e.g., "$userId")
1081
+ * @param routeData - The route data containing the request schema for parameter validation
1082
+ * @param req - The request object containing the actual parameter values in req.data
1083
+ * @param sqlParams - Array to which parameter values are appended (modified by reference)
1084
+ * @returns The value with parameter keywords replaced by '?' placeholders, or the original value if unchanged
1085
+ * @throws {RsError} If a parameter keyword is found that doesn't exist in the route's request schema
1086
+ */
1073
1087
  protected replaceLocalParamKeywords(value: string | number, routeData: RouteData, req: RsRequest<unknown>, sqlParams: string[]): string | number;
1074
1088
  protected replaceGlobalParamKeywords(value: string | number, routeData: RouteData, req: RsRequest<unknown>, sqlParams: string[]): string | number;
1075
1089
  abstract generateDatabaseSchemaFromSchema(schema: ResturaSchema): string;
@@ -1102,6 +1116,16 @@ declare class PsqlEngine extends SqlEngine {
1102
1116
  protected createNestedSelect(req: RsRequest<unknown>, schema: ResturaSchema, item: ResponseData, routeData: StandardRouteData, sqlParams: string[]): string;
1103
1117
  protected executeCreateRequest(req: RsRequest<unknown>, routeData: StandardRouteData, schema: ResturaSchema): Promise<DynamicObject>;
1104
1118
  protected executeGetRequest(req: RsRequest<unknown>, routeData: StandardRouteData, schema: ResturaSchema): Promise<DynamicObject | any[]>;
1119
+ /**
1120
+ * Executes an update request. The request will pull out the id and baseSyncVersion from the request body.
1121
+ * (If Present) The baseSyncVersion is used to check if the record has been modified since the last sync.
1122
+ * If the update fails because the baseSyncVersion has changed, a conflict error will be thrown.
1123
+ * IDs can not be updated using this method.
1124
+ * @param req - The request object.
1125
+ * @param routeData - The route data object.
1126
+ * @param schema - The schema object.
1127
+ * @returns The response object.
1128
+ */
1105
1129
  protected executeUpdateRequest(req: RsRequest<unknown>, routeData: StandardRouteData, schema: ResturaSchema): Promise<DynamicObject>;
1106
1130
  protected executeDeleteRequest(req: RsRequest<unknown>, routeData: StandardRouteData, schema: ResturaSchema): Promise<boolean>;
1107
1131
  protected generateJoinStatements(req: RsRequest<unknown>, joins: JoinData[], baseTable: string, routeData: StandardRouteData | CustomRouteData, schema: ResturaSchema, sqlParams: string[]): string;
@@ -1157,9 +1181,10 @@ declare function insertObjectQuery(table: string, obj: DynamicObject): string;
1157
1181
  * @param table Table name to update the object in
1158
1182
  * @param obj Data to update in the table
1159
1183
  * @param whereStatement Where clause to determine which rows to update
1184
+ * @param incrementSyncVersion Whether to increment the syncVersion column
1160
1185
  * @returns the query to update the object in the table
1161
1186
  */
1162
- declare function updateObjectQuery(table: string, obj: DynamicObject, whereStatement: string): string;
1187
+ declare function updateObjectQuery(table: string, obj: DynamicObject, whereStatement: string, incrementSyncVersion?: boolean): string;
1163
1188
  declare function isValueNumber(value: unknown): value is number;
1164
1189
  /**
1165
1190
  * This method is used to format a query and escape user input.
package/dist/index.js CHANGED
@@ -1791,15 +1791,18 @@ INSERT INTO "${table}" (${columns})
1791
1791
  query = query.replace(/'(\?)'/, "?");
1792
1792
  return query;
1793
1793
  }
1794
- function updateObjectQuery(table, obj, whereStatement) {
1794
+ function updateObjectQuery(table, obj, whereStatement, incrementSyncVersion = false) {
1795
1795
  const setArray = [];
1796
1796
  for (const i in obj) {
1797
1797
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1798
1798
  }
1799
+ if (incrementSyncVersion) {
1800
+ setArray.push(`"syncVersion" = "syncVersion" + 1`);
1801
+ }
1799
1802
  return `
1800
- UPDATE ${escapeColumnName(table)}
1801
- SET ${setArray.join(", ")} ${whereStatement}
1802
- RETURNING *`;
1803
+ UPDATE ${escapeColumnName(table)}
1804
+ SET ${setArray.join(", ")} ${whereStatement}
1805
+ RETURNING *`;
1803
1806
  }
1804
1807
  function isValueNumber2(value) {
1805
1808
  return !isNaN(Number(value));
@@ -1835,9 +1838,9 @@ var PsqlConnection = class {
1835
1838
  const startTime = process.hrtime();
1836
1839
  try {
1837
1840
  const response = await this.query(queryMetadata + formattedQuery, options);
1838
- this.logSqlStatement(formattedQuery, options, meta, startTime);
1839
1841
  if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1840
1842
  else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1843
+ this.logSqlStatement(formattedQuery, options, meta, startTime);
1841
1844
  return response.rows[0];
1842
1845
  } catch (error) {
1843
1846
  this.logSqlStatement(formattedQuery, options, meta, startTime);
@@ -2042,6 +2045,20 @@ var SqlEngine = class {
2042
2045
  returnValue = this.replaceGlobalParamKeywords(returnValue, routeData, req, sqlParams);
2043
2046
  return returnValue;
2044
2047
  }
2048
+ /**
2049
+ * Replaces local parameter keywords (e.g., $paramName) in SQL query strings with '?' placeholders
2050
+ * and adds the corresponding parameter values to the sqlParams array for parameterized queries.
2051
+ *
2052
+ * Validates that each parameter keyword exists in the route's request schema before processing.
2053
+ * If the value is not a string or routeData has no request schema, returns the value unchanged.
2054
+ *
2055
+ * @param value - The string or number value that may contain parameter keywords (e.g., "$userId")
2056
+ * @param routeData - The route data containing the request schema for parameter validation
2057
+ * @param req - The request object containing the actual parameter values in req.data
2058
+ * @param sqlParams - Array to which parameter values are appended (modified by reference)
2059
+ * @returns The value with parameter keywords replaced by '?' placeholders, or the original value if unchanged
2060
+ * @throws {RsError} If a parameter keyword is found that doesn't exist in the route's request schema
2061
+ */
2045
2062
  replaceLocalParamKeywords(value, routeData, req, sqlParams) {
2046
2063
  if (!routeData.request) return value;
2047
2064
  const data = req.data;
@@ -2618,9 +2635,19 @@ var PsqlEngine = class extends SqlEngine {
2618
2635
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
2619
2636
  }
2620
2637
  }
2638
+ /**
2639
+ * Executes an update request. The request will pull out the id and baseSyncVersion from the request body.
2640
+ * (If Present) The baseSyncVersion is used to check if the record has been modified since the last sync.
2641
+ * If the update fails because the baseSyncVersion has changed, a conflict error will be thrown.
2642
+ * IDs can not be updated using this method.
2643
+ * @param req - The request object.
2644
+ * @param routeData - The route data object.
2645
+ * @param schema - The schema object.
2646
+ * @returns The response object.
2647
+ */
2621
2648
  async executeUpdateRequest(req, routeData, schema) {
2622
2649
  const sqlParams = [];
2623
- const { id, ...bodyNoId } = req.body;
2650
+ const { id, baseSyncVersion, ...bodyNoId } = req.body;
2624
2651
  const table = schema.database.find((item) => {
2625
2652
  return item.name === routeData.table;
2626
2653
  });
@@ -2628,6 +2655,8 @@ var PsqlEngine = class extends SqlEngine {
2628
2655
  if (table.columns.find((column) => column.name === "modifiedOn")) {
2629
2656
  bodyNoId.modifiedOn = (/* @__PURE__ */ new Date()).toISOString();
2630
2657
  }
2658
+ let incrementSyncVersion = false;
2659
+ if (table.columns.find((column) => column.name === "syncVersion")) incrementSyncVersion = true;
2631
2660
  for (const assignment of routeData.assignments) {
2632
2661
  const column = table.columns.find((column2) => column2.name === assignment.name);
2633
2662
  if (!column) continue;
@@ -2636,9 +2665,36 @@ var PsqlEngine = class extends SqlEngine {
2636
2665
  bodyNoId[assignmentEscaped] = Number(assignment.value);
2637
2666
  else bodyNoId[assignmentEscaped] = assignment.value;
2638
2667
  }
2639
- const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2640
- const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
2641
- await this.psqlConnectionPool.queryOne(query, [...sqlParams], req.requesterDetails);
2668
+ let whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2669
+ const originalWhereClause = whereClause;
2670
+ const originalSqlParams = [...sqlParams];
2671
+ if (baseSyncVersion) {
2672
+ const syncVersionCheck = whereClause ? `${whereClause} AND "syncVersion" = ?` : `"syncVersion" = ?`;
2673
+ sqlParams.push(baseSyncVersion.toString());
2674
+ whereClause = syncVersionCheck;
2675
+ }
2676
+ const query = updateObjectQuery(routeData.table, bodyNoId, whereClause, incrementSyncVersion);
2677
+ try {
2678
+ await this.psqlConnectionPool.queryOne(query, [...sqlParams], req.requesterDetails);
2679
+ } catch (error) {
2680
+ if (!baseSyncVersion || !(error instanceof RsError) || error.err !== "NOT_FOUND") throw error;
2681
+ let isConflict = false;
2682
+ try {
2683
+ await this.psqlConnectionPool.queryOne(
2684
+ `SELECT 1 FROM "${routeData.table}" ${originalWhereClause};`,
2685
+ originalSqlParams,
2686
+ req.requesterDetails
2687
+ );
2688
+ isConflict = true;
2689
+ } catch {
2690
+ }
2691
+ if (isConflict)
2692
+ throw new RsError(
2693
+ "CONFLICT",
2694
+ "The record has been modified since the baseSyncVersion value was provided."
2695
+ );
2696
+ throw error;
2697
+ }
2642
2698
  return this.executeGetRequest(req, routeData, schema);
2643
2699
  }
2644
2700
  async executeDeleteRequest(req, routeData, schema) {