From 93dd70e2507a8ad8ca5879b221ff898539c82009 Mon Sep 17 00:00:00 2001 From: Andrew Dodson Date: Mon, 2 Jun 2025 11:44:32 +0100 Subject: [PATCH 1/8] fix(field_expression): support [FUNCNAME](field AS [UPPERCASE|String|Number]) (#385) --- src/utils/unwrap_field.js | 2 +- test/specs/field_format.spec.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/unwrap_field.js b/src/utils/unwrap_field.js index b562d217..25559642 100644 --- a/src/utils/unwrap_field.js +++ b/src/utils/unwrap_field.js @@ -46,7 +46,7 @@ export default function unwrap_field(expression, allowValue = true) { // Split out comma variables while ( (int_m = str.match( - /^(.*)((,|AS)\s*(?(?["'])?[\s\w%./-]*\k))$/ + /^(.*)((,|\sAS)\s*(?(?["'])?[\s\w%./-]*\k))$/ )) ) { /* diff --git a/test/specs/field_format.spec.js b/test/specs/field_format.spec.js index b75753ef..4677b820 100644 --- a/test/specs/field_format.spec.js +++ b/test/specs/field_format.spec.js @@ -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.'], From 3daf8944042d07a7df4093a6179ff8af2976ef5b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:05:00 +0000 Subject: [PATCH 2/8] chore(deps): update dependency @5app/semantic-release-config to v2 --- package-lock.json | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10af5ec1..0fb57d83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,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", @@ -51,7 +51,9 @@ "license": "ISC" }, "node_modules/@5app/semantic-release-config": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@5app/semantic-release-config/-/semantic-release-config-2.0.0.tgz", + "integrity": "sha512-DWEDLMo+ppIh1Ce3zpdF1gv+/udAeQw/hTo5/X9A3gii7n8NCr2ORYVQCUFvQsNGy3DhfqxY1Gd0PTFMdJCCUw==", "dev": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index aaf85067..8f7b94e9 100644 --- a/package.json +++ b/package.json @@ -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", From 9eb150579a26d4e59ae71f47956e0bb5e58782e9 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Thu, 17 Jul 2025 19:33:57 +0000 Subject: [PATCH 3/8] chore(release): 0.97.2 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb986f5..0f3b4c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) diff --git a/package-lock.json b/package-lock.json index 0fb57d83..f19b6bc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dare", - "version": "0.97.1", + "version": "0.97.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dare", - "version": "0.97.1", + "version": "0.97.2", "license": "MIT", "dependencies": { "semver-compare": "^1.0.0", diff --git a/package.json b/package.json index 8f7b94e9..8508d466 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dare", - "version": "0.97.1", + "version": "0.97.2", "description": "Database to REST, REST to Database", "type": "module", "main": "./src/index.js", From 2e5d363f79cebc043bb6f4e044e5ac77975c5604 Mon Sep 17 00:00:00 2001 From: Andrew Dodson <947163+MrSwitch@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:39:32 +0100 Subject: [PATCH 4/8] feat(field.setFunction): enable JSON_MERGE_PATCH definition (#412) --- src/index.js | 25 +++++++++++++++++----- test/integration/json.spec.js | 39 ++++++++++++++++++++++++++++++++++- test/integration/run.sh | 2 +- test/specs/patch.spec.js | 35 +++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index 331d8377..9fed8670 100644 --- a/src/index.js +++ b/src/index.js @@ -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 @@ -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], @@ -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}` ); } @@ -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, @@ -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); @@ -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}; } /** diff --git a/test/integration/json.spec.js b/test/integration/json.spec.js index 28962c81..c2eb3442 100644 --- a/test/integration/json.spec.js +++ b/test/integration/json.spec.js @@ -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 @@ -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}); + }); }); diff --git a/test/integration/run.sh b/test/integration/run.sh index de5272d1..ac363608 100644 --- a/test/integration/run.sh +++ b/test/integration/run.sh @@ -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) diff --git a/test/specs/patch.spec.js b/test/specs/patch.spec.js index 61524c21..b7d65f65 100644 --- a/test/specs/patch.spec.js +++ b/test/specs/patch.spec.js @@ -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'; @@ -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 () => { From fdda99541c812dd5273b450f37d57c1cedc9f5a8 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Fri, 18 Jul 2025 09:41:59 +0000 Subject: [PATCH 5/8] chore(release): 0.98.0 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3b4c3c..5b2e0104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index f19b6bc4..cb20b90a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dare", - "version": "0.97.2", + "version": "0.98.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dare", - "version": "0.97.2", + "version": "0.98.0", "license": "MIT", "dependencies": { "semver-compare": "^1.0.0", diff --git a/package.json b/package.json index 8508d466..018c673a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dare", - "version": "0.97.2", + "version": "0.98.0", "description": "Database to REST, REST to Database", "type": "module", "main": "./src/index.js", From 6a1d9cac6bbf21351bdefac49d732e1de647d0a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:03:05 +0100 Subject: [PATCH 6/8] chore(deps): update dependency node to v7.1.1 (#414) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a3ea91ec..2d298e6e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,7 +25,7 @@ executors: # Orbs orbs: - node: circleci/node@7.1.0 + node: circleci/node@7.1.1 ################################ # Jobs From 63072c86530a2d0ebae65ba7e9a95f5bb5ac45f2 Mon Sep 17 00:00:00 2001 From: Andrew Dodson <947163+MrSwitch@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:22:07 +0100 Subject: [PATCH 7/8] fix(datetime): always allow a range (#415) --- src/format/reducer_conditions.js | 19 ++++++++++++------- test/specs/format_request.spec.js | 7 +++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/format/reducer_conditions.js b/src/format/reducer_conditions.js index 2f93c7e7..a39ae038 100644 --- a/src/format/reducer_conditions.js +++ b/src/format/reducer_conditions.js @@ -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; @@ -171,7 +171,7 @@ function prepCondition({ value, sql_alias, table_schema, - operators: operators?.replace('-', ''), + operators: operators.replace('-', ''), conditional_operators_in_value, dareInstance, }) @@ -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) { @@ -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 = diff --git a/test/specs/format_request.spec.js b/test/specs/format_request.spec.js index 705c30a3..abb54d83 100644 --- a/test/specs/format_request.spec.js +++ b/test/specs/format_request.spec.js @@ -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'}, From d8969e1037b30a29c37dd42967da0048def96d73 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Fri, 8 Aug 2025 15:24:55 +0000 Subject: [PATCH 8/8] chore(release): 0.98.1 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b2e0104..bfb8d7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) diff --git a/package-lock.json b/package-lock.json index cb20b90a..3d33eaae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dare", - "version": "0.98.0", + "version": "0.98.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dare", - "version": "0.98.0", + "version": "0.98.1", "license": "MIT", "dependencies": { "semver-compare": "^1.0.0", diff --git a/package.json b/package.json index 018c673a..cecb51e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dare", - "version": "0.98.0", + "version": "0.98.1", "description": "Database to REST, REST to Database", "type": "module", "main": "./src/index.js",