Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ executors:
# Orbs

orbs:
node: circleci/node@7.1.0
node: circleci/node@7.1.1

################################
# Jobs
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
## [0.98.1](https://github.com/5app/dare/compare/v0.98.0...v0.98.1) (2025-08-08)


### Bug Fixes

* **datetime:** always allow a range ([#415](https://github.com/5app/dare/issues/415)) ([63072c8](https://github.com/5app/dare/commit/63072c86530a2d0ebae65ba7e9a95f5bb5ac45f2))

# [0.98.0](https://github.com/5app/dare/compare/v0.97.2...v0.98.0) (2025-07-18)


### Features

* **field.setFunction:** enable JSON_MERGE_PATCH definition ([#412](https://github.com/5app/dare/issues/412)) ([2e5d363](https://github.com/5app/dare/commit/2e5d363f79cebc043bb6f4e044e5ac77975c5604))

## [0.97.2](https://github.com/5app/dare/compare/v0.97.1...v0.97.2) (2025-07-17)


### Bug Fixes

* **field_expression:** support [FUNCNAME](field AS [UPPERCASE|String|Number]) ([#385](https://github.com/5app/dare/issues/385)) ([93dd70e](https://github.com/5app/dare/commit/93dd70e2507a8ad8ca5879b221ff898539c82009))

## [0.97.1](https://github.com/5app/dare/compare/v0.97.0...v0.97.1) (2025-07-03)


Expand Down
10 changes: 6 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dare",
"version": "0.97.1",
"version": "0.98.1",
"description": "Database to REST, REST to Database",
"type": "module",
"main": "./src/index.js",
Expand Down Expand Up @@ -54,7 +54,7 @@
],
"devDependencies": {
"@5app/prettier-config": "^1.0.4",
"@5app/semantic-release-config": "^1.1.0",
"@5app/semantic-release-config": "^2.0.0",
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
"@types/mocha": "^10.0.10",
Expand Down
19 changes: 12 additions & 7 deletions src/format/reducer_conditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,17 @@ function prepCondition({
value,
sql_alias,
table_schema,
operators,
operators = '',
conditional_operators_in_value,
dareInstance,
}) {
const {engine} = dareInstance;

// Does it have a negative comparison operator?
const negate = operators?.includes('-');
const negate = operators.includes('-');

// Does it have a FullText comparison operator
const isFullText = operators?.includes('*');
const isFullText = operators.includes('*');

// Set a handly NOT value
const NOT = negate ? raw('NOT ') : empty;
Expand Down Expand Up @@ -171,7 +171,7 @@ function prepCondition({
value,
sql_alias,
table_schema,
operators: operators?.replace('-', ''),
operators: operators.replace('-', ''),
conditional_operators_in_value,
dareInstance,
})
Expand All @@ -190,6 +190,11 @@ function prepCondition({
// Format date time values
if (type === 'datetime') {
value = formatDateTime(value);

// NOTE: Could we just return SQL from formatDateTime instead of ensuring an implicit range?
if (!operators.includes('~')) {
operators += '~'; // Add range operator
}
}
// @ts-ignore
else if (type === 'date' && value instanceof Date) {
Expand Down Expand Up @@ -255,16 +260,16 @@ function sqlCondition({
const IS_POSTGRES = engine.startsWith('postgres');

// Does it have a negative comparison operator?
const negate = operators?.includes('-');
const negate = operators.includes('-');

// Set a handly NOT value
const NOT = negate ? raw('NOT ') : empty;

// Does it have a Likey comparison operator
const isLikey = operators?.includes('%');
const isLikey = operators.includes('%');

// Does it have a Range comparison operator
const isRange = operators?.includes('~');
const isRange = operators.includes('~');

// Allow conditional likey operator in value
const allow_conditional_likey_operator_in_value =
Expand Down
25 changes: 20 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import response_handler, {responseRowHandler} from './response_handler.js';
* @property {boolean} [writeable=true] - Whether this field is writeable
* @property {boolean} [required=false] - Whether this field is required
* @property {Handler} [handler] - Handler to generate the field value
* @property {(arg: {sql_field: string, field: string, value: any}) => * | SQL} [setFunction] - String defining a SQL function to wrap the value in when setting it
* @property {FieldAttributes} [get] - The get definition of this field
* @property {FieldAttributes} [post] - The post definition of this field
* @property {FieldAttributes} [patch] - The patch definition of this field
Expand Down Expand Up @@ -977,7 +978,8 @@ function prepareSQLSet({
* Get the real field in the db,
* And formatted value...
*/
const {field, value} = formatInputValue({
const {sql_field, value} = formatInputValue({
sql_alias,
tableSchema,
field: label,
value: body[label],
Expand All @@ -987,7 +989,7 @@ function prepareSQLSet({

// Replace value with a question using any mapped fieldName
assignments.push(
SQL`${raw((sql_alias ? `${sql_alias}.` : '') + dareInstance.identifierWrapper(field))} = ${value}`
SQL`${raw(sql_field)} = ${value}`
);
}

Expand Down Expand Up @@ -1043,15 +1045,17 @@ Dare.prototype.onDuplicateKeysUpdate = function onDuplicateKeysUpdate(
* For a given field definition, return the db key (alias) and format the input it required
* @param {object} obj - Object
* @param {Schema} [obj.tableSchema={}] - An object containing the table schema
* @param {string} [obj.sql_alias=null] - SQL Alias for the table
* @param {string} obj.field - field identifier
* @param {*} obj.value - Given value
* @param {Function} [obj.validateInput] - Custom validation function
* @param {Dare} obj.dareInstance - Dare Instance
* @throws Will throw an error if the field is not writable
* @returns {{field: string, value: *}} A singular value which can be inserted
* @returns {{field: string, sql_field: string, value: *}} A singular value which can be inserted
*/
function formatInputValue({
tableSchema = {},
sql_alias = null,
field,
value,
validateInput,
Expand All @@ -1073,7 +1077,7 @@ function formatInputValue({
fieldAttributes = null;
}

const {alias, writeable, type} = fieldAttributes || {};
const {alias, writeable, type, setFunction} = fieldAttributes || {};

// Execute custom field validation
validateInput?.(fieldAttributes, field, value);
Expand Down Expand Up @@ -1135,7 +1139,18 @@ function formatInputValue({
field = alias;
}

return {field, value};
// Format the field
const sql_field = (sql_alias ? `${sql_alias}.` : '') + dareInstance.identifierWrapper(field);

/**
* Format the set value
*/
if (setFunction) {
// If the insertWrapper is defined, use it to format the value
value = setFunction({value, field, sql_field});
}

return {field, sql_field, value};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/utils/unwrap_field.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@

// Split out comma variables
while (
(int_m = str.match(
/^(.*)((,|AS)\s*(?<suffix>(?<quote>["'])?[\s\w%./-]*\k<quote>))$/
/^(.*)((,|\sAS)\s*(?<suffix>(?<quote>["'])?[\s\w%./-]*\k<quote>))$/
))

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with ',' and with many repetitions of '\t'.
) {
/*
* If unquoted parameter
Expand Down
39 changes: 38 additions & 1 deletion test/integration/json.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import SQL from 'sql-template-tag';
import SQL, {raw} from 'sql-template-tag';
import defaultAPI from './helpers/api.js';

// Connect to db
Expand Down Expand Up @@ -162,4 +162,41 @@ describe('Working with JSON DataType', () => {

assert.strictEqual(noMatch, null);
});

it('JSON fields should be patchable with a setFunction definition', async function () {

if (DB_ENGINE?.startsWith('postgres')) {
this.skip();
return;
}

// Update the user settings with a setFunction
dare.options.models.users.schema.settings.patch = {
setFunction({sql_field, value}) {
return SQL`JSON_MERGE_PATCH(${raw(sql_field)}, ${value})`;
}
};

// Insert intial settings
const settings = {a: 1, b: 0};

await dare.post('users', {username, settings});

// Patch the settings
const newSettings = {b: 2, c: 3};

await dare.patch({
table: 'users',
filter: {username},
body: {settings: newSettings},
});

const resp = await dare.get({
table: 'users',
fields: ['settings'],
filter: {username},
});

assert.deepStrictEqual(resp.settings, {...settings, ...newSettings});
});
});
2 changes: 1 addition & 1 deletion test/integration/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cd "$INTEGRATION_TEST_DIR" || exit 1

DB_ROOT_USER="root"
DB_ROOT_PASSWORD="test_pass"
DB_ENGINE=${DB_ENGINE:-mysql:5.7.40}
DB_ENGINE=${DB_ENGINE:-mysql:8.0.23}

DB_ENGINE_NAME=$(echo $DB_ENGINE | cut -d: -f1)

Expand Down
6 changes: 6 additions & 0 deletions test/specs/field_format.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ describe('utils/field_format', () => {
['nested.field', 'label'],
],

// Function: CAST
[
['CAST(field AS CHAR)', 'label', 'tbl'],
['CAST(tbl.field AS CHAR)', 'label'],
],

// If the expression defines a nested field, take that away from the prefix label address
[
['SUM(nested.field)', 'count', 'nested', 'nested.'],
Expand Down
7 changes: 7 additions & 0 deletions test/specs/format_request.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,13 @@ describe('format_request', () => {
'(NOT a.datetime > ? OR a.datetime IS NULL)',
['1981-12-05T00:00:00'],
],
[
// Should always expand datetime fields
{'datetime': '1981-12-05..1981-12-06'},
'a.datetime BETWEEN ? AND ?',
['1981-12-05T00:00:00', '1981-12-06T23:59:59'],
noCondOperators,
],
[{prop: '1981-12-05..'}, 'a.prop > ?', ['1981-12-05']],
[
{prop: '1970-01-01..1981-12-05'},
Expand Down
35 changes: 35 additions & 0 deletions test/specs/patch.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Dare from '../../src/index.js';
import sqlEqual from '../lib/sql-equal.js';

import DareError from '../../src/utils/error.js';
import SQL, {raw} from 'sql-template-tag';

const id = 1;
const name = 'name';
Expand Down Expand Up @@ -170,6 +171,40 @@ describe('patch', () => {
});
});
});

it('should apply schema.field.setFunction', () => {

dare.options.models = {
test: {
schema: {
meta: {
type: 'json',
setFunction({sql_field, value}) {
return SQL`JSON_MERGE_PATCH(${raw(sql_field)}, ${value})`;
},
},
},
},
};

const meta = {key: 'value'};

dare.execute = async ({sql, values}) => {
// Limit: 1
sqlEqual(
sql,
'UPDATE test a SET a.`meta` = JSON_MERGE_PATCH(a.`meta`, ?) WHERE a.id = ? LIMIT ?'
);
expect(values).to.deep.equal([JSON.stringify(meta), id, 1]);
return {success: true};
};

return dare.patch({
table: 'test',
filter: {id},
body: {meta},
});
});
});

it('should apply the request.limit', async () => {
Expand Down