diff --git a/Jenkinsfile b/Jenkinsfile index 5020e2fb..b149762d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,7 +125,7 @@ pipeline { agent none triggers { - parameterizedCron(env.BRANCH_NAME == "develop" ? "00 02 * * * % regressions=true" : "") + parameterizedCron(env.BRANCH_NAME == "develop" ? "00 05 * * * % regressions=true" : "") } parameters { diff --git a/etc/test-setup-users.js b/etc/test-setup-users.js index 87d8c7fd..9024024e 100644 --- a/etc/test-setup-users.js +++ b/etc/test-setup-users.js @@ -1,5 +1,5 @@ /* -* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ var valcheck = require('core-util-is'); @@ -68,6 +68,16 @@ function setupUsers(manager, done) { 'privilege-name': 'xdmp-get-session-field', action: 'http://marklogic.com/xdmp/privileges/xdmp-get-session-field', kind: 'execute' + }, + { + 'privilege-name': 'xdmp-lock-acquire', + action: 'http://marklogic.com/xdmp/privileges/xdmp-lock-acquire', + kind: 'execute' + }, + { + 'privilege-name': 'xdmp-lock-release', + action: 'http://marklogic.com/xdmp/privileges/xdmp-lock-release', + kind: 'execute' } ] } diff --git a/lib/plan-builder-base.js b/lib/plan-builder-base.js index 1e802640..c983e446 100644 --- a/lib/plan-builder-base.js +++ b/lib/plan-builder-base.js @@ -136,6 +136,13 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) { throw new Error( 'bm25LengthWeight must be a number' ); + case 'fragment': + if (['document', 'properties', 'locks', 'any'].includes(value)) { + return true; + } + throw new Error( + `${argLabel(funcName, paramName, argPos)} fragment can only be 'document', 'properties', 'locks', or 'any'` + ); default: return false; }}); diff --git a/lib/plan-builder-generated.js b/lib/plan-builder-generated.js index e833e29e..04f38879 100755 --- a/lib/plan-builder-generated.js +++ b/lib/plan-builder-generated.js @@ -8993,7 +8993,7 @@ fromSQL(...args) { * @since 2.1.1 * @param { PlanSearchQuery } [query] - Qualifies and establishes the scores for a set of documents. The query can be a cts:query or a string as a shortcut for a cts:word-query. * @param { XsString } [qualifierName] - Specifies a name for qualifying the column names. - * @param { PlanSearchOption } [option] - Similar to the options of cts:search, supplies the 'scoreMethod' key with a value of 'logtfidf', 'logtf', 'simple', 'zero', 'random', or 'bm25' to specify the method for assigning a score to matched documents or supplies the 'qualityWeight' key with a numeric value to specify a multiplier for the quality contribution to the score. 'logtfidf' is the default score method and the results are ordered by score by default. Specify a value between 0 (exclusive) and 1 (inclusive) for bm25LengthWeight if 'bm25' scoring method is used. + * @param { PlanSearchOption } [option] - Similar to the options of cts:search, supplies the 'scoreMethod' key with a value of 'logtfidf', 'logtf', 'simple', 'zero', 'random', or 'bm25' to specify the method for assigning a score to matched documents or supplies the 'qualityWeight' key with a numeric value to specify a multiplier for the quality contribution to the score. 'logtfidf' is the default score method and the results are ordered by score by default. Specify a value between 0 (exclusive) and 1 (inclusive) for bm25LengthWeight if 'bm25' scoring method is used. As of MLS 12.1, supplies the 'fragment' key with a value of 'document' (default), 'properties', 'locks', or 'any' to specify which document fragment type to search. Note: on servers earlier than MLS 12.1, the 'fragment' option is silently ignored and all fragment types are searched. * @returns { planBuilder.AccessPlan } */ fromSearchDocs(...args) { @@ -9012,7 +9012,7 @@ fromSearchDocs(...args) { * @param { PlanSearchQuery } [query] - Qualifies and establishes the scores for a set of documents. The query can be a cts:query or a string as a shortcut for a cts:word-query. The fragments are not filtered to ensure they match the query, but instead selected in the same manner as "unfiltered" cts:search operations. * @param { PlanExprColName } [columns] - Specifies which of the available columns to include in the rows. The available columns include the metrics for relevance ('confidence', 'fitness', 'quality', and 'score') and fragmentId for the document identifier. By default, the rows have the fragmentId and score columns. To rename a column, use op:as specifying the new name for an op:col with the old name. * @param { XsString } [qualifierName] - Specifies a name for qualifying the column names. - * @param { PlanSearchOption } [option] - Similar to the options of cts:search, supplies the 'scoreMethod' key with a value of 'logtfidf', 'logtf', 'simple', 'zero', 'random', or 'bm25' to specify the method for assigning a score to matched documents or supplies the 'qualityWeight' key with a numeric value to specify a multiplier for the quality contribution to the score. Specify a value between 0 (exclusive) and 1 (inclusive) for bm25LengthWeight if 'bm25' scoring method is used. + * @param { PlanSearchOption } [option] - Similar to the options of cts:search, supplies the 'scoreMethod' key with a value of 'logtfidf', 'logtf', 'simple', 'zero', 'random', or 'bm25' to specify the method for assigning a score to matched documents or supplies the 'qualityWeight' key with a numeric value to specify a multiplier for the quality contribution to the score. Specify a value between 0 (exclusive) and 1 (inclusive) for bm25LengthWeight if 'bm25' scoring method is used. As of MLS 12.1, supplies the 'fragment' key with a value of 'document' (default), 'properties', 'locks', or 'any' to specify which document fragment type to search. Note: on servers earlier than MLS 12.1, the 'fragment' option is silently ignored and all fragment types are searched. * @returns { planBuilder.AccessPlan } */ fromSearch(...args) { diff --git a/package-lock.json b/package-lock.json index 1b55c7ff..4fbf96d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "mocha": "^11.7.5", "mocha-junit-reporter": "2.2.1", "moment": "2.30.1", - "sanitize-html": "2.17.0", + "sanitize-html": "^2.17.4", "should": "13.2.3", "stream-to-array": "2.3.0", "typescript": "5.7.2" @@ -131,15 +131,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -185,9 +185,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -198,7 +198,7 @@ "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -727,11 +727,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/bare-events": { "version": "2.8.1", @@ -811,13 +814,16 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -1172,6 +1178,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -1276,9 +1289,9 @@ } }, "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1839,9 +1852,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -1986,9 +1999,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -2580,9 +2593,9 @@ } }, "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -2595,8 +2608,21 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/human-signals": { @@ -3088,6 +3114,16 @@ "node": ">= 10.13.0" } }, + "node_modules/launder": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/launder/-/launder-1.7.1.tgz", + "integrity": "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dayjs": "^1.11.7" + } + }, "node_modules/lead": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", @@ -3158,9 +3194,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -3562,16 +3598,6 @@ "node": ">=12" } }, - "node_modules/mocha/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4064,9 +4090,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4090,9 +4116,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -4452,16 +4478,17 @@ } }, "node_modules/sanitize-html": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", - "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", + "version": "2.17.4", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.4.tgz", + "integrity": "sha512-2HW7v2ol/uAM7sX4hbD8Z59OGWmAPrvjL8E71UWlBcj6m+kcF6ilQBLny+cIgY214QJeJT5tQuxKKqX0SQqjGQ==", "dev": true, "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", + "htmlparser2": "^10.1.0", "is-plain-object": "^5.0.0", + "launder": "^1.7.1", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } @@ -4509,9 +4536,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/package.json b/package.json index e01bad67..960827d3 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "mocha": "^11.7.5", "mocha-junit-reporter": "2.2.1", "moment": "2.30.1", - "sanitize-html": "2.17.0", + "sanitize-html": "^2.17.4", "should": "13.2.3", "stream-to-array": "2.3.0", "typescript": "5.7.2" @@ -86,18 +86,19 @@ "ansi-styles": "4.3.0", "ansi-regex": "5.0.1", "braces": "3.0.3", - "brace-expansion": "2.0.2", + "brace-expansion": "5.0.6", "chalk": "4.1.2", "color-convert": "3.1.0", "color-name": "2.0.0", "cross-spawn": "7.0.6", "debug": "4.3.6", + "diff": "9.0.0", "glob": "12.0.0", "glob-parent": "6.0.2", "markdown-it": "14.1.1", "minimatch": "10.2.4", "semver": "7.5.3", - "serialize-javascript": "7.0.4", + "serialize-javascript": "7.0.5", "strip-ansi": "6.0.0", "supports-color": "7.2.0", "tar-fs": "2.1.4", diff --git a/test-app/src/main/ml-config/security/roles/rest-evaluator.json b/test-app/src/main/ml-config/security/roles/rest-evaluator.json index 9d315236..3da382b0 100644 --- a/test-app/src/main/ml-config/security/roles/rest-evaluator.json +++ b/test-app/src/main/ml-config/security/roles/rest-evaluator.json @@ -65,6 +65,16 @@ "privilege-name": "xdmp-xslt-invoke", "action": "http://marklogic.com/xdmp/privileges/xslt-invoke", "kind": "execute" + }, + { + "privilege-name": "xdmp-lock-acquire", + "action": "http://marklogic.com/xdmp/privileges/xdmp-lock-acquire", + "kind": "execute" + }, + { + "privilege-name": "xdmp-lock-release", + "action": "http://marklogic.com/xdmp/privileges/xdmp-lock-release", + "kind": "execute" } ] } diff --git a/test-basic/optic-cts-param-test.js b/test-basic/optic-cts-param-test.js index 53881db5..298fa1a3 100644 --- a/test-basic/optic-cts-param-test.js +++ b/test-basic/optic-cts-param-test.js @@ -18,23 +18,30 @@ const valcheck = require('core-util-is'); const testconfig = require('../etc/test-config.js'); const marklogic = require('../'); +const testlib = require('../etc/test-lib'); +let serverConfiguration = {}; -// Allow overriding connection info via environment or direct config -const connInfo = { - host: process.env.ML_TEST_HOST || testconfig.testHost || 'localhost', - port: process.env.ML_TEST_PORT || 8000, - database: process.env.ML_TEST_DATABASE || 'Documents', - authType: process.env.ML_TEST_AUTH_TYPE || 'digest', - user: process.env.ML_TEST_USER || testconfig.restWriterConnection.user, - password: process.env.ML_TEST_PASSWORD || testconfig.restWriterConnection.password, -}; - -const db = marklogic.createDatabaseClient(connInfo); +const db = marklogic.createDatabaseClient(testconfig.restWriterConnection); const op = marklogic.planBuilder; describe('cts.param integration tests (MLE-27883)', function() { this.timeout(10000); // Allow 10 seconds for server queries + before(function(done) { + try { + testlib.findServerConfiguration(serverConfiguration); + setTimeout(() => { done(); }, 3000); + } catch(error) { + done(error); + } + }); + + before(function() { + if (serverConfiguration.serverVersion < 12.1) { + this.skip(); + } + }); + // ────────────────────────────────────────────────────────────────────────────── // Test: collectionQuery with cts.param binding // ────────────────────────────────────────────────────────────────────────────── diff --git a/test-basic/plan-search.js b/test-basic/plan-search.js index 20fc3022..215643f3 100644 --- a/test-basic/plan-search.js +++ b/test-basic/plan-search.js @@ -1,5 +1,5 @@ /* -* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ 'use strict'; @@ -308,4 +308,205 @@ describe('search', function() { }).catch(error => done(error)); }); }); + + describe('fragment option tests for fromSearch', function() { + const setupXquery = ` + xquery version "1.0-ml"; + let $jsondoc1 := object-node {"AllDataTypes": array-node {object-node {"word":"dog"}, object-node {"rank":1}, object-node {"score":4}}} + let $jsondoc2 := object-node {"AllDataTypes": array-node {object-node {"word":"cat"}, object-node {"rank":2}, object-node {"score":5}}} + let $jsondoc3 := object-node {"AllDataTypes": array-node {object-node {"word":"duck"}, object-node {"rank":3}, object-node {"score":6}}} + return ( + xdmp:document-insert("range-prop-1.json", $jsondoc1, xdmp:default-permissions(), ("elemCol","jsondoc-range")), + xdmp:document-insert("range-prop-2.json", $jsondoc2, xdmp:default-permissions(), ("elemCol","jsondoc-range")), + xdmp:document-insert("range-prop-3.json", $jsondoc3, xdmp:default-permissions(), ("elemCol","jsondoc-range")), + xdmp:document-set-properties("range-prop-1.json", (opticfragmentpropvalue)), + xdmp:lock-acquire("range-prop-1.json", "exclusive", "0", "dog rose", xs:unsignedLong(120)), + xdmp:lock-acquire("range-prop-2.json", "exclusive", "0", "cat tulip", xs:unsignedLong(120)), + xdmp:lock-acquire("range-prop-3.json", "exclusive", "0", "duck lily", xs:unsignedLong(120)) + ) + `; + + const teardownReleaseLocks = ` + xquery version "1.0-ml"; + ( + xdmp:lock-release("range-prop-1.json"), + xdmp:lock-release("range-prop-2.json"), + xdmp:lock-release("range-prop-3.json") + ) + `; + + const teardownDeleteDocs = ` + xquery version "1.0-ml"; + ( + xdmp:document-delete("range-prop-1.json"), + xdmp:document-delete("range-prop-2.json"), + xdmp:document-delete("range-prop-3.json") + ) + `; + + before(function(done) { + if (serverConfiguration.serverVersion < 12.1) { + this.skip(); + } + pbb.dbWriter.xqueryEval(setupXquery).result() + .then(() => done()) + .catch(done); + }); + + after(function(done) { + pbb.dbWriter.xqueryEval(teardownReleaseLocks).result() + .then(() => pbb.dbWriter.xqueryEval(teardownDeleteDocs).result()) + .then(() => done()) + .catch(done); + }); + + // TC0: No fragment option — default behavior searches document content (same as fragment:'document') + it('TC0: fromSearch without fragment option should search document content by default', function(done) { + execPlan( + p.fromSearch( + p.cts.wordQuery('dog') + ) + .joinDocAndUri('doc', 'uri', p.fragmentIdCol('fragmentId')) + .orderBy('uri') + .select(['uri', 'doc']) + ).then(function(response) { + const output = getResults(response); + assert(output.length === 1, 'Expected exactly 1 document containing "dog" with default fragment'); + assert(output[0].uri.value === 'range-prop-1.json', 'Expected range-prop-1.json'); + assert(output[0].doc.type === 'object', 'Expected default fragment to return JSON document'); + assert(output[0].doc.value.AllDataTypes[0].word === 'dog', 'Expected word "dog" in document content'); + done(); + }).catch(done); + }); + + // TC0b: Invalid fragment value → client-side error (no server call needed) + it('TC0b: should throw error for invalid fragment value', function() { + assert.throws(function() { + p.fromSearch( + p.cts.wordQuery('dog'), null, null, { fragment: 'unknown' } + ); + }, /fragment can only be/); + }); + + // TC1: fragment:'locks' — doc joined from locks fragment must be XML containing 'lock-type' + it('TC1: fromSearch with fragment:locks should find documents by lock token', function(done) { + execPlan( + p.fromSearch( + p.cts.locksFragmentQuery(p.cts.wordQuery('dog')), + null, null, { fragment: 'locks' } + ) + .joinDocAndUri('doc', 'uri', p.fragmentIdCol('fragmentId')) + .orderBy('uri') + .select(['uri', 'doc']) + ).then(function(response) { + const output = getResults(response); + assert(output.length === 1, 'Expected exactly 1 result from locks fragment'); + assert(output[0].uri.value === 'range-prop-1.json', 'Expected range-prop-1.json'); + assert(output[0].doc.type === 'element', 'Expected lock doc to be XML element'); + assert(output[0].doc.value.includes('lock-type'), 'Expected lock-type element in lock document'); + done(); + }).catch(done); + }); + + // TC2: fragment:'properties' — doc joined from properties fragment must be XML containing the property value + it('TC2: fromSearch with fragment:properties should find doc by its properties', function(done) { + execPlan( + p.fromSearch( + p.cts.wordQuery('opticfragmentpropvalue'), + null, null, { fragment: 'properties' } + ) + .joinDocAndUri('doc', 'uri', p.fragmentIdCol('fragmentId')) + .orderBy('uri') + .select(['uri', 'doc']) + ).then(function(response) { + const output = getResults(response); + assert(output.length === 1, 'Expected exactly 1 result from properties fragment'); + assert(output[0].uri.value === 'range-prop-1.json', 'Expected range-prop-1.json'); + assert(output[0].doc.type === 'element', 'Expected properties doc to be XML element'); + assert(output[0].doc.value.includes('opticfragmentpropvalue'), 'Expected property value in properties document'); + done(); + }).catch(done); + }); + + // TC3: fragment:'any' — returns all fragment types; verify both XML (lock/properties) and JSON (document) rows present + it('TC3: fromSearch with fragment:any should return results across fragment types', function(done) { + execPlan( + p.fromSearch( + p.cts.locksFragmentQuery(p.cts.wordQuery('dog')), + null, null, { fragment: 'any' } + ) + .joinDocAndUri('doc', 'uri', p.fragmentIdCol('fragmentId')) + .orderBy('uri') + .select(['uri', 'doc']) + ).then(function(response) { + const output = getResults(response); + assert(output.length > 1, 'Expected multiple rows (all fragment types) with fragment:any'); + const types = output.map(row => row.doc.type); + assert(types.includes('element'), 'Expected at least one XML fragment (lock or properties)'); + assert(types.includes('object'), 'Expected at least one JSON document fragment'); + done(); + }).catch(done); + }); + + // TC4: fragment:'document' — doc must be JSON containing the word 'dog' + it('TC4: fromSearch with fragment:document should find documents by content word', function(done) { + execPlan( + p.fromSearch( + p.cts.wordQuery('dog'), + null, null, { fragment: 'document' } + ) + .joinDocAndUri('doc', 'uri', p.fragmentIdCol('fragmentId')) + .orderBy('uri') + .select(['uri', 'doc']) + ).then(function(response) { + const output = getResults(response); + assert(output.length === 1, 'Expected exactly 1 document containing "dog"'); + assert(output[0].uri.value === 'range-prop-1.json', 'Expected range-prop-1.json'); + assert(output[0].doc.type === 'object', 'Expected document fragment to be JSON'); + assert(output[0].doc.value.AllDataTypes[0].word === 'dog', 'Expected word "dog" in document content'); + done(); + }).catch(done); + }); + + // TC5: explain() on a locks fragment plan should return a valid execution plan structure. + // Note: the server-side equivalent (TEST26) additionally exercises plan:parse()/plan:execute() + // on the explain output, but the Node client has no equivalent of those functions. + it('TC5: explain() on a locks fragment plan should return a valid plan structure', function(done) { + const plan = p.fromSearch( + p.cts.locksFragmentQuery(p.cts.wordQuery('dog')), + null, null, { fragment: 'locks' } + ) + .joinDocAndUri('doc', 'uri', p.fragmentIdCol('fragmentId')) + .orderBy('uri') + .select(['uri', 'doc']); + + pbb.explainPlan(plan) + .then(function(output) { + assert(output.node === 'plan', 'Expected explain output to have node:"plan"'); + assert(output.expr != null, 'Expected expr to be present in explain output'); + done(); + }) + .catch(done); + }); + + // TC6: fromSearchDocs with fragment:'locks' — confirms fromSearchDocs honors the fragment option (MLS 12.1+) + it('TC6: fromSearchDocs with fragment:locks should find documents by lock token', function(done) { + execPlan( + p.fromSearchDocs( + p.cts.locksFragmentQuery(p.cts.wordQuery('dog')), + null, + { fragment: 'locks' } + ) + .orderBy('uri') + .select(['uri', 'doc']) + ).then(function(response) { + const output = getResults(response); + assert(output.length === 1, 'Expected exactly 1 result from fromSearchDocs with fragment:locks'); + assert(output[0].uri.value === 'range-prop-1.json', 'Expected range-prop-1.json'); + assert(output[0].doc.type === 'element', 'Expected lock doc to be XML element'); + assert(output[0].doc.value.includes('lock-type'), 'Expected lock-type element in lock document'); + done(); + }).catch(done); + }); + }); });