This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 2578f62ebf Extend the update()/delete() DSL with safer toggles, plus
two bugs fixed along the way (#2043)
2578f62ebf is described below
commit 2578f62ebf6d66a80974f69faadd1239e64d418f
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Fri Sep 18 14:18:13 2026 +0530
Extend the update()/delete() DSL with safer toggles, plus two bugs fixed
along the way (#2043)
1) Added ifExists() and first() toggles to the update() DSL, so update()
can now handle a record that might not exist and an entity that can
legitimately have more than one matching row, plus a setIfEmpty option that
preserves existing values instead of nulling them for empty-string input fields.
2) Converted four call sites, updateProductCategory,
CategoryContentServicesScript.updateContent, updateContentSEOForProduct, and
addRejectedReasonImageManagement, to use these new toggles instead of the old
manual query-then-store code.
3) Re-converted updatePartyTaxAuthInfo to use update() with
setIfEmpty=false, which fixes the exact gap that forced this same conversion to
be reverted in an earlier round.
4) Made first() actually safe to call with a raw service parameters map,
one that still has extra keys like userLogin or locale in it.
5) Fixed the same missing safety in delete(), which was more serious there,
since a raw or mismatched map used to go straight to a database delete with no
filtering at all, meaning it could silently wipe an entire table instead of
throwing an error, and this fix also caught a live example of that exact risk
inside duplicateWorkEffort's cleanup code against the WorkEffortAssoc entity.
6) Added orderBy() support to the update() DSL, so update() can now pick
the most recent matching record instead of just any matching record.
7) Converted ExampleServicesScript.createExampleStatus() to use the new
orderBy() support, so it now expires the previous status through the update()
DSL instead of a manual query and store.
8) Fixed a real, pre-existing bug in duplicateWorkEffortAssoc() that looked
up the wrong entity name internally and crashed every single time a work effort
was duplicated along with its associations, notes, contents, or assignment
rates.
Testing: every conversion and every bug fix has a dedicated regression
test, and the affected component suites (accounting, product, workeffort,
example, service) all pass, along with checkstyleMain and codenarcMain.
---
applications/accounting/ofbiz-component.xml | 1 +
.../tax/TaxAuthorityServicesScript.groovy | 10 ++-
.../accounting/tax/test/TaxAuthorityTests.groovy | 75 ++++++++++++++++
.../testdef/TaxAuthorityTests.xml} | 18 +---
.../category/CategoryContentServicesScript.groovy | 7 +-
.../product/category/CategoryServicesScript.groovy | 7 +-
.../ImageManagementServicesScript.groovy | 5 +-
.../product/ProductContentServicesScript.groovy | 27 ++----
.../product/product/test/CategoryTests.groovy | 56 ++++++++++++
.../product/test/ImageManagementTests.groovy | 67 +++++++++++++++
.../product/test/ProductContentTests.groovy | 64 ++++++++++++++
applications/product/testdef/CatalogTests.xml | 8 ++
.../workeffort/WorkEffortServicesScript.groovy | 2 +-
.../workeffort/test/WorkEffortTests.groovy | 30 +++++++
.../service/engine/EntityDeleteBuilder.groovy | 22 ++++-
.../service/engine/EntityUpdateBuilder.groovy | 74 +++++++++++++++-
.../test/engine/EntityDeleteBuilderTest.groovy | 99 ++++++++++++++++++++++
.../test/engine/EntityUpdateBuilderTest.groovy | 86 +++++++++++++++++++
framework/service/testdef/servicetests.xml | 6 ++
19 files changed, 603 insertions(+), 61 deletions(-)
diff --git a/applications/accounting/ofbiz-component.xml
b/applications/accounting/ofbiz-component.xml
index a691cf1409..a4e54bcf96 100644
--- a/applications/accounting/ofbiz-component.xml
+++ b/applications/accounting/ofbiz-component.xml
@@ -64,6 +64,7 @@ under the License.
<test-suite loader="main" location="testdef/invoicetests.xml"/>
<test-suite loader="main" location="testdef/fixedassettests.xml"/>
<test-suite loader="main" location="testdef/ratetests.xml"/>
+ <test-suite loader="main" location="testdef/TaxAuthorityTests.xml"/>
<webapp name="accounting"
title="Accounting"
diff --git
a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/tax/TaxAuthorityServicesScript.groovy
b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/tax/TaxAuthorityServicesScript.groovy
index a961b711b4..b95bd354ff 100644
---
a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/tax/TaxAuthorityServicesScript.groovy
+++
b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/tax/TaxAuthorityServicesScript.groovy
@@ -20,6 +20,7 @@ package org.apache.ofbiz.accounting.tax
import org.apache.ofbiz.base.util.UtilDateTime
import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.service.ServiceErrorException
import java.util.regex.Pattern
@@ -46,10 +47,11 @@ Map updatePartyTaxAuthInfo() {
String errorMesg = validatePartyTaxIdInline()
boolean isTaxIdValid = !errorMesg
require(isTaxIdValid, errorMesg)
- GenericValue partyAuthInfo =
from('PartyTaxAuthInfo').where(parameters).queryOne()
- require(partyAuthInfo as boolean, 'PartyTaxAuthInfo not found for the
given parameters')
- partyAuthInfo.setNonPKFields(parameters, false)
- partyAuthInfo.store()
+ try {
+ update('PartyTaxAuthInfo').where(parameters).set(parameters, false)
+ } catch (ServiceErrorException e) {
+ fail('PartyTaxAuthInfo not found for the given parameters')
+ }
return success()
}
diff --git
a/applications/accounting/src/test/groovy/org/apache/ofbiz/accounting/tax/test/TaxAuthorityTests.groovy
b/applications/accounting/src/test/groovy/org/apache/ofbiz/accounting/tax/test/TaxAuthorityTests.groovy
new file mode 100644
index 0000000000..b73aa95177
--- /dev/null
+++
b/applications/accounting/src/test/groovy/org/apache/ofbiz/accounting/tax/test/TaxAuthorityTests.groovy
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.accounting.tax.test
+
+import org.apache.ofbiz.base.util.UtilDateTime
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.testtools.JunitJupiterTest
+import org.apache.ofbiz.testtools.JupiterTestHelper
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.Test
+
+import java.sql.Timestamp
+
+@JunitJupiterTest
+class TaxAuthorityTests implements JupiterTestHelper {
+
+ @Test
+ @Order(1)
+ void
testUpdatePartyTaxAuthInfoPreservesEmptyFieldsWhileApplyingRealChanges() {
+ String partyId = userLogin.partyId
+ String taxAuthGeoId = 'USA'
+ String taxAuthPartyId = partyId
+ Timestamp fromDate = UtilDateTime.nowTimestamp()
+
+ Map createTaxAuthorityCtx = [taxAuthGeoId: taxAuthGeoId,
taxAuthPartyId: taxAuthPartyId, userLogin: userLogin]
+ Map createTaxAuthorityResult =
dispatcher.runSync('createTaxAuthority', createTaxAuthorityCtx)
+ assert ServiceUtil.isSuccess(createTaxAuthorityResult)
+
+ Map createCtx = [partyId: partyId, taxAuthGeoId: taxAuthGeoId,
taxAuthPartyId: taxAuthPartyId,
+ fromDate: fromDate, isExempt: 'Y', isNexus: 'N',
userLogin: userLogin]
+ Map createResult = dispatcher.runSync('createPartyTaxAuthInfo',
createCtx)
+ assert ServiceUtil.isSuccess(createResult)
+
+ // isExempt is passed as an empty string here -- with setIfEmpty=false
this must NOT null out
+ // the 'Y' set above, while isNexus (a real, non-empty change) must
still apply.
+ Map updateCtx = [partyId: partyId, taxAuthGeoId: taxAuthGeoId,
taxAuthPartyId: taxAuthPartyId,
+ fromDate: fromDate, isExempt: '', isNexus: 'Y',
userLogin: userLogin]
+ Map updateResult = dispatcher.runSync('updatePartyTaxAuthInfo',
updateCtx)
+ assert ServiceUtil.isSuccess(updateResult)
+
+ GenericValue partyTaxAuthInfo = from('PartyTaxAuthInfo')
+ .where('partyId', partyId, 'taxAuthGeoId', taxAuthGeoId,
'taxAuthPartyId', taxAuthPartyId, 'fromDate', fromDate)
+ .queryOne()
+ assert partyTaxAuthInfo.isNexus == 'Y'
+ assert partyTaxAuthInfo.isExempt == 'Y'
+ }
+
+ @Test
+ @Order(2)
+ void testUpdatePartyTaxAuthInfoNotFoundPreservesOriginalMessage() {
+ Map updateCtx = [partyId: 'DoesNotExist12345', taxAuthGeoId: 'USA',
taxAuthPartyId: 'DoesNotExist12345',
+ fromDate: UtilDateTime.nowTimestamp(), isNexus: 'Y',
userLogin: userLogin]
+ Map updateResult = dispatcher.runSync('updatePartyTaxAuthInfo',
updateCtx)
+ assert ServiceUtil.isError(updateResult)
+ assert updateResult.errorMessage == 'PartyTaxAuthInfo not found for
the given parameters'
+ }
+
+}
diff --git a/applications/product/testdef/CatalogTests.xml
b/applications/accounting/testdef/TaxAuthorityTests.xml
similarity index 58%
copy from applications/product/testdef/CatalogTests.xml
copy to applications/accounting/testdef/TaxAuthorityTests.xml
index f228134eb7..9e4e71334e 100644
--- a/applications/product/testdef/CatalogTests.xml
+++ b/applications/accounting/testdef/TaxAuthorityTests.xml
@@ -18,23 +18,11 @@ specific language governing permissions and limitations
under the License.
-->
-<test-suite suite-name="catalogtests"
+<test-suite suite-name="taxauthoritytests"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/test-suite.xsd">
- <test-case case-name="loadProductPriceTestData">
- <entity-xml action="load"
entity-xml-url="component://product/testdef/data/ProductPriceTestData.xml"/>
- </test-case>
-
- <test-case case-name="productPrice-tests">
- <jupiter-test-suite
class-name="org.apache.ofbiz.product.product.test.ProductPriceTests"/>
- </test-case>
-
- <test-case case-name="loadCategoryTestData">
- <entity-xml action="load"
entity-xml-url="component://product/testdef/data/CategoryTestData.xml"/>
- </test-case>
-
- <test-case case-name="category-tests">
- <jupiter-test-suite
class-name="org.apache.ofbiz.product.product.test.CategoryTests"/>
+ <test-case case-name="taxAuthority-tests">
+ <jupiter-test-suite
class-name="org.apache.ofbiz.accounting.tax.test.TaxAuthorityTests"/>
</test-case>
</test-suite>
diff --git
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryContentServicesScript.groovy
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryContentServicesScript.groovy
index c03d3f8be6..cc4ba490a7 100644
---
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryContentServicesScript.groovy
+++
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryContentServicesScript.groovy
@@ -92,11 +92,8 @@ Map updateContent(String param, String typeId) {
.queryList()
if (productCategoryContents) {
Map productCategoryContent =
EntityUtil.getFirst(productCategoryContents)
- Map electronicText =
from('ElectronicText').where('dataResourceId',
productCategoryContent.dataResourceId).queryOne()
- if (electronicText) {
- electronicText.textData = parameters."${param}"
- electronicText.store()
- }
+ update('ElectronicText').where([dataResourceId:
productCategoryContent.dataResourceId])
+ .ifExists().set([textData: parameters."${param}"])
} else {
Map createTextContentMap = [
productCategoryId: parameters.productCategoryId,
diff --git
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryServicesScript.groovy
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryServicesScript.groovy
index b7720f8fee..63275044ab 100644
---
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryServicesScript.groovy
+++
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/category/CategoryServicesScript.groovy
@@ -91,11 +91,8 @@ Map updateProductCategory() {
if (!ServiceUtil.isSuccess(res)) {
return res
}
- GenericValue lookedUpValue =
from('ProductCategory').where(parameters).queryOne()
- if (lookedUpValue) { // fails in framework integration tests only, data is
in ecommerce
- lookedUpValue.setNonPKFields(parameters)
- lookedUpValue.store()
- }
+ // fails in framework integration tests only, data is in ecommerce
+ update('ProductCategory').where(parameters).ifExists().set(parameters)
return success()
}
diff --git
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/imagemanagement/ImageManagementServicesScript.groovy
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/imagemanagement/ImageManagementServicesScript.groovy
index dcb8b47f56..de7fb02cac 100644
---
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/imagemanagement/ImageManagementServicesScript.groovy
+++
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/imagemanagement/ImageManagementServicesScript.groovy
@@ -356,9 +356,8 @@ Map addRejectedReasonImageManagement() {
break
}
update('Content').where(parameters).set([description: description])
- GenericValue productContent = from('ProductContent').where(contentId:
parameters.contentId, productContentTypeId: 'IMAGE').queryFirst()
- productContent.thruDate = nowTimestamp
- productContent.store()
+ update('ProductContent').where(contentId: parameters.contentId,
productContentTypeId: 'IMAGE')
+ .first().set([thruDate: nowTimestamp])
return success()
}
diff --git
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/product/ProductContentServicesScript.groovy
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/product/ProductContentServicesScript.groovy
index 550599adb3..842ade3a8f 100644
---
a/applications/product/src/main/groovy/org/apache/ofbiz/product/product/product/ProductContentServicesScript.groovy
+++
b/applications/product/src/main/groovy/org/apache/ofbiz/product/product/product/ProductContentServicesScript.groovy
@@ -151,13 +151,8 @@ Map updateContentSEOForProduct() {
productContentTypeId: 'PAGE_TITLE')
.queryFirst()
if (productContent) {
- GenericValue electronicText = from('ElectronicText')
- .where(dataResourceId: productContent.dataResourceId)
- .queryOne()
- if (electronicText) {
- electronicText.textData = parameters.title
- electronicText.store()
- }
+ update('ElectronicText').where(dataResourceId:
productContent.dataResourceId)
+ .ifExists().set([textData: parameters.title])
} else {
Map createTextContentMap = [productId: parameters.productId,
productContentTypeId: 'PAGE_TITLE',
@@ -171,13 +166,8 @@ Map updateContentSEOForProduct() {
productContentTypeId: 'META_KEYWORD')
.queryFirst()
if (productContent) {
- GenericValue electronicText = from('ElectronicText')
- .where(dataResourceId: productContent.dataResourceId)
- .queryOne()
- if (electronicText) {
- electronicText.textData = parameters.metaKeyword
- electronicText.store()
- }
+ update('ElectronicText').where(dataResourceId:
productContent.dataResourceId)
+ .ifExists().set([textData: parameters.metaKeyword])
} else {
Map createTextContentMap = [productId: parameters.productId,
productContentTypeId: 'META_KEYWORD',
@@ -191,13 +181,8 @@ Map updateContentSEOForProduct() {
productContentTypeId: 'META_DESCRIPTION')
.queryFirst()
if (productContent) {
- GenericValue electronicText = from('ElectronicText')
- .where(dataResourceId: productContent.dataResourceId)
- .queryOne()
- if (electronicText) {
- electronicText.textData = parameters.metaDescription
- electronicText.store()
- }
+ update('ElectronicText').where(dataResourceId:
productContent.dataResourceId)
+ .ifExists().set([textData: parameters.metaDescription])
} else {
Map createTextContentMap = [productId: parameters.productId,
productContentTypeId:
'META_DESCRIPTION',
diff --git
a/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/CategoryTests.groovy
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/CategoryTests.groovy
index b4358ca441..dbfc3bb93b 100644
---
a/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/CategoryTests.groovy
+++
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/CategoryTests.groovy
@@ -74,5 +74,61 @@ class CategoryTests implements JupiterTestHelper {
assert
productCategoryMemberList.containsAll(serviceResult.productCategoryMembers)
}
+ @Test
+ @Order(3)
+ void testUpdateProductCategory() {
+ String productCategoryId = testParams.productCategoryId ?: 'TPC'
+ String newDescription = 'Updated Long Test Product Category
Description'
+ Map serviceCtx = [
+ productCategoryId: productCategoryId,
+ productCategoryTypeId: 'TEST_CATEGORY',
+ longDescription: newDescription,
+ userLogin: userLogin
+ ]
+ Map serviceResult = dispatcher.runSync('updateProductCategory',
serviceCtx)
+ assert ServiceUtil.isSuccess(serviceResult)
+
+ GenericValue productCategory =
from('ProductCategory').where('productCategoryId', productCategoryId).queryOne()
+ assert productCategory.longDescription == newDescription
+ }
+
+ @Test
+ @Order(4)
+ void testUpdateProductCategoryNotFoundIsSilentNoOp() {
+ Map serviceCtx = [
+ productCategoryId: 'DoesNotExist12345',
+ productCategoryTypeId: 'TEST_CATEGORY',
+ longDescription: 'should never be applied',
+ userLogin: userLogin
+ ]
+ Map serviceResult = dispatcher.runSync('updateProductCategory',
serviceCtx)
+ assert ServiceUtil.isSuccess(serviceResult)
+
+ GenericValue productCategory =
from('ProductCategory').where('productCategoryId',
'DoesNotExist12345').queryOne()
+ assert productCategory == null
+ }
+
+ @Test
+ @Order(5)
+ void testUpdateContentSEOForCategoryUpdatesExistingTitle() {
+ String productCategoryId = testParams.productCategoryId ?: 'TPCP'
+
+ Map firstCallCtx = [productCategoryId: productCategoryId, title:
'First SEO Title', userLogin: userLogin]
+ Map firstCallResult =
dispatcher.runSync('updateContentSEOForCategory', firstCallCtx)
+ assert ServiceUtil.isSuccess(firstCallResult)
+
+ Map secondCallCtx = [productCategoryId: productCategoryId, title:
'Second SEO Title', userLogin: userLogin]
+ Map secondCallResult =
dispatcher.runSync('updateContentSEOForCategory', secondCallCtx)
+ assert ServiceUtil.isSuccess(secondCallResult)
+
+ GenericValue productCategoryContent =
from('ProductCategoryContentAndInfo')
+ .where('productCategoryId', productCategoryId,
'prodCatContentTypeId', 'PAGE_TITLE')
+ .queryFirst()
+ GenericValue electronicText = from('ElectronicText')
+ .where('dataResourceId', productCategoryContent.dataResourceId)
+ .queryOne()
+ assert electronicText.textData == 'Second SEO Title'
+ }
+
}
diff --git
a/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ImageManagementTests.groovy
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ImageManagementTests.groovy
new file mode 100644
index 0000000000..00a1433307
--- /dev/null
+++
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ImageManagementTests.groovy
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.product.product.test
+
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.testtools.JunitJupiterTest
+import org.apache.ofbiz.testtools.JupiterTestHelper
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.Test
+
+@JunitJupiterTest
+class ImageManagementTests implements JupiterTestHelper {
+
+ @Test
+ @Order(1)
+ void testAddRejectedReasonImageManagementSetsThruDate() {
+ Map createProductCtx = [productTypeId: 'FINISHED_GOOD', internalName:
'Test Image Product', userLogin: userLogin]
+ Map createProductResult = dispatcher.runSync('createProduct',
createProductCtx)
+ assert ServiceUtil.isSuccess(createProductResult)
+ String productId = createProductResult.productId
+
+ // createProductContent's own contentId is a required
(non-auto-generating) INOUT pk
+ // attribute, so create the Content record explicitly first via the
generic, genuinely
+ // auto-generating createContent service, then link it.
+ Map createContentCtx = [contentName: 'Test Rejected Reason Image',
userLogin: userLogin]
+ Map createContentResult = dispatcher.runSync('createContent',
createContentCtx)
+ assert ServiceUtil.isSuccess(createContentResult)
+ String contentId = createContentResult.contentId
+
+ Map createProductContentCtx = [productId: productId, contentId:
contentId,
+ productContentTypeId: 'IMAGE',
userLogin: userLogin]
+ Map createProductContentResult =
dispatcher.runSync('createProductContent', createProductContentCtx)
+ assert ServiceUtil.isSuccess(createProductContentResult)
+
+ GenericValue productContentBefore = from('ProductContent')
+ .where('productId', productId, 'productContentTypeId',
'IMAGE', 'contentId', contentId)
+ .queryOne()
+ assert productContentBefore.thruDate == null
+
+ Map addRejectedReasonCtx = [contentId: contentId, description:
'RETAKE_PHOTO', userLogin: userLogin]
+ Map addRejectedReasonResult =
dispatcher.runSync('addRejectedReasonImageManagement', addRejectedReasonCtx)
+ assert ServiceUtil.isSuccess(addRejectedReasonResult)
+
+ GenericValue productContentAfter = from('ProductContent')
+ .where('productId', productId, 'productContentTypeId',
'IMAGE', 'contentId', contentId)
+ .queryOne()
+ assert productContentAfter.thruDate != null
+ }
+
+}
diff --git
a/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ProductContentTests.groovy
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ProductContentTests.groovy
new file mode 100644
index 0000000000..64bec89143
--- /dev/null
+++
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ProductContentTests.groovy
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.product.product.test
+
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.testtools.JunitJupiterTest
+import org.apache.ofbiz.testtools.JupiterTestHelper
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.Test
+
+@JunitJupiterTest
+class ProductContentTests implements JupiterTestHelper {
+
+ @Test
+ @Order(1)
+ void testUpdateContentSEOForProductUpdatesExistingText() {
+ Map createProductCtx = [productTypeId: 'FINISHED_GOOD', internalName:
'Test SEO Product', userLogin: userLogin]
+ Map createProductResult = dispatcher.runSync('createProduct',
createProductCtx)
+ assert ServiceUtil.isSuccess(createProductResult)
+ String productId = createProductResult.productId
+
+ Map firstCallCtx = [productId: productId, title: 'First Title',
metaKeyword: 'First Keyword',
+ metaDescription: 'First Description', userLogin:
userLogin]
+ Map firstCallResult = dispatcher.runSync('updateContentSEOForProduct',
firstCallCtx)
+ assert ServiceUtil.isSuccess(firstCallResult)
+
+ Map secondCallCtx = [productId: productId, title: 'Second Title',
metaKeyword: 'Second Keyword',
+ metaDescription: 'Second Description',
userLogin: userLogin]
+ Map secondCallResult =
dispatcher.runSync('updateContentSEOForProduct', secondCallCtx)
+ assert ServiceUtil.isSuccess(secondCallResult)
+
+ assert textDataFor(productId, 'PAGE_TITLE') == 'Second Title'
+ assert textDataFor(productId, 'META_KEYWORD') == 'Second Keyword'
+ assert textDataFor(productId, 'META_DESCRIPTION') == 'Second
Description'
+ }
+
+ private String textDataFor(String productId, String productContentTypeId) {
+ GenericValue productContent = from('ProductContentAndInfo')
+ .where('productId', productId, 'productContentTypeId',
productContentTypeId)
+ .queryFirst()
+ GenericValue electronicText = from('ElectronicText')
+ .where('dataResourceId', productContent.dataResourceId)
+ .queryOne()
+ return electronicText.textData
+ }
+
+}
diff --git a/applications/product/testdef/CatalogTests.xml
b/applications/product/testdef/CatalogTests.xml
index f228134eb7..8abc1ef5e4 100644
--- a/applications/product/testdef/CatalogTests.xml
+++ b/applications/product/testdef/CatalogTests.xml
@@ -37,4 +37,12 @@ under the License.
<test-case case-name="category-tests">
<jupiter-test-suite
class-name="org.apache.ofbiz.product.product.test.CategoryTests"/>
</test-case>
+
+ <test-case case-name="productContent-tests">
+ <jupiter-test-suite
class-name="org.apache.ofbiz.product.product.test.ProductContentTests"/>
+ </test-case>
+
+ <test-case case-name="imageManagement-tests">
+ <jupiter-test-suite
class-name="org.apache.ofbiz.product.product.test.ImageManagementTests"/>
+ </test-case>
</test-suite>
diff --git
a/applications/workeffort/src/main/groovy/org/apache/ofbiz/workeffort/workeffort/workeffort/WorkEffortServicesScript.groovy
b/applications/workeffort/src/main/groovy/org/apache/ofbiz/workeffort/workeffort/workeffort/WorkEffortServicesScript.groovy
index efed762390..3b4bc2b1a7 100644
---
a/applications/workeffort/src/main/groovy/org/apache/ofbiz/workeffort/workeffort/workeffort/WorkEffortServicesScript.groovy
+++
b/applications/workeffort/src/main/groovy/org/apache/ofbiz/workeffort/workeffort/workeffort/WorkEffortServicesScript.groovy
@@ -384,7 +384,7 @@ Map duplicateWorkEffort() {
*/
void duplicateWorkEffortAssoc(String relationEntityName, String
oldWorkEffortId,
String workEffortId, String relationFieldName =
'workEffortId') {
- ModelEntity modelEntity = delegator.getModelEntity(relationFieldName)
+ ModelEntity modelEntity = delegator.getModelEntity(relationEntityName)
EntityQuery entities = from(relationEntityName)
.where((relationFieldName): oldWorkEffortId)
if (modelEntity.getField('fromDate')) {
diff --git
a/applications/workeffort/src/test/groovy/org/apache/ofbiz/workeffort/workeffort/test/WorkEffortTests.groovy
b/applications/workeffort/src/test/groovy/org/apache/ofbiz/workeffort/workeffort/test/WorkEffortTests.groovy
index 0d9766e648..1dbbee0833 100644
---
a/applications/workeffort/src/test/groovy/org/apache/ofbiz/workeffort/workeffort/test/WorkEffortTests.groovy
+++
b/applications/workeffort/src/test/groovy/org/apache/ofbiz/workeffort/workeffort/test/WorkEffortTests.groovy
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
@JunitJupiterTest
+@SuppressWarnings('MethodCount')
class WorkEffortTests implements JupiterTestHelper {
@Test
@@ -734,4 +735,33 @@ class WorkEffortTests implements JupiterTestHelper {
assert rateAmount
}
+ @Test
+ @Order(31)
+ void testDuplicateWorkEffortWithAssocsAndNotes() {
+ // duplicateWorkEffortAssoc() looked up its model entity by the
relation FIELD name
+ // ("workEffortIdTo") instead of the relation ENTITY name
("WorkEffortAssoc"), so
+ // getModelEntity() always returned null and
modelEntity.getField('fromDate') threw a
+ // NullPointerException -- this path
(duplicateWorkEffortAssocs/Notes/Contents/AssignmentRates
+ // == 'Y') always failed before the fix, regardless of which relation
was being duplicated.
+ String oldWorkEffortId = testParams.oldWorkEffortId ?:
'TestWorkeffort-3'
+ Map serviceCtx = [
+ oldWorkEffortId: oldWorkEffortId,
+ duplicateWorkEffortAssocs: 'Y',
+ duplicateWorkEffortNotes: 'Y',
+ userLogin: userLogin,
+ ]
+ Map serviceResult = dispatcher.runSync('duplicateWorkEffort',
serviceCtx)
+ assert ServiceUtil.isSuccess(serviceResult)
+ String workEffortId = serviceResult.workEffortId
+ assert workEffortId
+
+ List<GenericValue> duplicatedAssocs =
from('WorkEffortAssoc').where('workEffortIdTo', workEffortId).queryList()
+ assert duplicatedAssocs
+ assert duplicatedAssocs[0].workEffortIdFrom == 'TestWorkeffort-2'
+
+ List<GenericValue> duplicatedNotes =
from('WorkEffortNote').where('workEffortId', workEffortId).queryList()
+ assert duplicatedNotes
+ assert duplicatedNotes[0].noteId == 'TestNote-1'
+ }
+
}
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
index f8949ed522..d9e440a154 100644
---
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
+++
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityDeleteBuilder.groovy
@@ -20,12 +20,20 @@ package org.apache.ofbiz.service.engine
import org.apache.ofbiz.entity.Delegator
import org.apache.ofbiz.entity.GenericEntityException
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.service.ServiceErrorException
/**
* Fluent write-side helper backing {@link GroovyBaseScript#delete(String)}.
*
- * Usage: {@code delete('SomeEntity').where(fields)} — bulk-removes every
matching record and
- * returns the number of rows removed.
+ * Usage: {@code delete('SomeEntity').where(fields)} — bulk-removes every
record matching the given
+ * fields and returns the number of rows removed. The given fields are not
required to be the full
+ * primary key: deleting by a real, non-PK field (e.g. a foreign key shared by
several child rows) is
+ * a legitimate, commonly-used bulk-delete pattern. {@code where()} narrows
the given fields down to
+ * the entity's own field names itself (like {@code set()} does via {@code
setNonPKFields()}), so
+ * it's safe to pass a raw service {@code parameters} map. Throws {@link
ServiceErrorException} if
+ * none of the given fields are real fields on the entity at all, rather than
silently building a
+ * condition-less delete that would remove every record in the entity.
*/
class EntityDeleteBuilder {
@@ -37,8 +45,14 @@ class EntityDeleteBuilder {
this.entityName = entityName
}
- int where(Map<String, Object> fields) throws GenericEntityException {
- return delegator.removeByAnd(entityName, fields)
+ int where(Map<String, Object> fields) throws GenericEntityException,
ServiceErrorException {
+ GenericValue entityFields = delegator.makeValue(entityName)
+ entityFields.setAllFields(fields, true, null, null)
+ if (entityFields.isEmpty()) {
+ throw new ServiceErrorException(
+ "Cannot delete ${entityName}: given fields include none of
its own fields" as String)
+ }
+ return delegator.removeByAnd(entityName, entityFields)
}
}
diff --git
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
index d0d9677afe..2acf529b42 100644
---
a/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
+++
b/framework/service/src/main/groovy/org/apache/ofbiz/service/engine/EntityUpdateBuilder.groovy
@@ -30,12 +30,25 @@ import org.apache.ofbiz.service.ServiceErrorException
* merges the given fields into it, stores it and returns the updated {@link
GenericValue}.
* Throws {@link ServiceErrorException} if no record matches {@code where()},
so a missing record
* fails fast instead of silently no-op'ing.
+ *
+ * {@code .ifExists()} suppresses that throw, returning {@code null} instead,
for sites that
+ * intentionally no-op on a missing record. {@code .first()} uses {@code
queryFirst()} instead of
+ * {@code queryOne()}, for sites where more than one record can legitimately
match; unlike {@code
+ * queryOne()}, {@code queryFirst()} does not narrow {@code where()} to PK
fields only, so {@code
+ * .first()} narrows it to the entity's own field names itself, making it just
as safe to pass a raw
+ * {@code parameters} map. {@code .orderBy()} passes through to the internal
{@code queryFirst()}/
+ * {@code queryOne()} lookup, for sites where "first" means "most recent"
rather than "arbitrary."
+ * {@code set(fields, false)} mirrors {@link GenericValue#setNonPKFields(Map,
boolean)}'s two-arg
+ * form, preserving existing field values instead of nulling them for
empty-string input fields.
*/
class EntityUpdateBuilder {
private final Delegator delegator
private final String entityName
private Map<String, Object> whereFields
+ private String orderByField
+ private boolean ifExists = false
+ private boolean first = false
EntityUpdateBuilder(Delegator delegator, String entityName) {
this.delegator = delegator
@@ -47,16 +60,71 @@ class EntityUpdateBuilder {
return this
}
- GenericValue set(Map<String, Object> fields) throws ServiceErrorException {
- GenericValue existing =
EntityQuery.use(delegator).from(entityName).where(whereFields).queryOne()
+ /**
+ * On no match, {@link #set(Map, boolean)} returns {@code null} instead of
throwing
+ * {@link ServiceErrorException} -- mirrors {@code queryOne()}'s own
null-on-no-match
+ * convention. For sites that intentionally no-op when the record doesn't
exist.
+ */
+ @SuppressWarnings('ConfusingMethodName')
+ EntityUpdateBuilder ifExists() {
+ this.ifExists = true
+ return this
+ }
+
+ /**
+ * Uses {@code queryFirst()} instead of {@code queryOne()} for the
internal lookup -- for sites
+ * where more than one record can legitimately match {@code where()}.
Unlike {@code queryOne()},
+ * {@code queryFirst()} does not narrow {@code where()} to PK fields only,
so {@link #set(Map,
+ * boolean)} narrows {@code where()} to the entity's own field names
itself before querying --
+ * safe to call with either explicit field names or a raw service {@code
parameters} map.
+ */
+ @SuppressWarnings('ConfusingMethodName')
+ EntityUpdateBuilder first() {
+ this.first = true
+ return this
+ }
+
+ /**
+ * The field to order the internal lookup by -- for {@code .first()} sites
where "first" means
+ * "most recent"/"lowest sequence" rather than an arbitrary match. Prefix
a field name with
+ * {@code -} for descending order, matching {@code
EntityQuery.orderBy()}'s own convention.
+ */
+ EntityUpdateBuilder orderBy(String orderByField) {
+ this.orderByField = orderByField
+ return this
+ }
+
+ GenericValue set(Map<String, Object> fields, boolean setIfEmpty = true)
throws ServiceErrorException {
+ EntityQuery query = EntityQuery.use(delegator).from(entityName)
+ if (first) {
+ // queryOne() narrows where() to PK fields internally
(searchPkOnly, the same mechanism
+ // setPKFields() uses); queryFirst() does not, so a raw service
parameters map (userLogin,
+ // locale, timeZone, ...) would otherwise blow up building a
condition on a column that
+ // isn't one of this entity's own fields. setAllFields(fields,
true, null, null) -- null
+ // pks means walk every field, not just PK -- is the same "walk
the entity's own fields,
+ // pull matching values out of the map" primitive set() already
uses below via
+ // setNonPKFields().
+ GenericValue entityWhereFields = delegator.makeValue(entityName)
+ entityWhereFields.setAllFields(whereFields, true, null, null)
+ query = query.where(entityWhereFields)
+ } else {
+ query = query.where(whereFields)
+ }
+ if (orderByField) {
+ query = query.orderBy(orderByField)
+ }
+ GenericValue existing = first ? query.queryFirst() : query.queryOne()
if (existing == null) {
+ if (ifExists) {
+ return null
+ }
throw new ServiceErrorException("No ${entityName} found matching
${whereFields}" as String)
}
// Like setNonPKFields(), not a raw Map.putAll(): walks the entity's
own non-PK fields and pulls
// matching values out of the given map, silently ignoring anything
else (e.g. userLogin, locale,
// timeZone commonly present in a raw service parameters map). This is
what makes it safe to call
// as update(entity).where(cond).set(parameters) -- the primary use
case this builder exists for.
- existing.setNonPKFields(fields)
+ existing.setNonPKFields(fields, setIfEmpty)
existing.store()
return existing
}
diff --git
a/framework/service/src/test/groovy/org/apache/ofbiz/service/test/engine/EntityDeleteBuilderTest.groovy
b/framework/service/src/test/groovy/org/apache/ofbiz/service/test/engine/EntityDeleteBuilderTest.groovy
new file mode 100644
index 0000000000..3f58d25ea7
--- /dev/null
+++
b/framework/service/src/test/groovy/org/apache/ofbiz/service/test/engine/EntityDeleteBuilderTest.groovy
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ofbiz.service.test.engine
+
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+import org.apache.ofbiz.service.ServiceErrorException
+import org.apache.ofbiz.service.engine.EntityDeleteBuilder
+import org.apache.ofbiz.testtools.JunitJupiterTest
+import org.apache.ofbiz.testtools.JupiterTestHelper
+import org.junit.jupiter.api.Test
+
+/**
+ * First time : ./gradlew 'ofbiz -l readers=seed,seed-initial -l
delegator=test'
+ * ./gradlew 'ofbiz -t component=service -t suitename=servicetests -t
case=engine-entity-delete-builder-tests'
+ */
+@JunitJupiterTest
+class EntityDeleteBuilderTest implements JupiterTestHelper {
+
+ @Test
+ void testWhereFiltersRawMapToEntityFieldsBeforeDelete() {
+ String keepId = 'EDB' + UUID.randomUUID().toString().replace('-',
'').take(16)
+ String deleteId = 'EDB' + UUID.randomUUID().toString().replace('-',
'').take(16)
+ delegator.create('TestingType', [testingTypeId: keepId, description:
'Keep'])
+ delegator.create('TestingType', [testingTypeId: deleteId, description:
'Delete me'])
+
+ // A raw service parameters map: the PK field, a real non-PK field
(its actual stored value,
+ // since non-PK fields legitimately participate in the delete
condition -- unlike update()'s
+ // set(), a delete()'s where() is a bulk-match, not a
find-then-modify), and non-entity keys
+ // (userLogin, locale) that must be silently ignored rather than
blowing up on a nonexistent
+ // column.
+ Map rawParameters = [testingTypeId: deleteId, description: 'Delete
me', userLogin: 'system', locale: 'en_US']
+
+ int rowsDeleted = new EntityDeleteBuilder(delegator,
'TestingType').where(rawParameters)
+
+ assert rowsDeleted == 1
+ assert from('TestingType').where('testingTypeId', deleteId).queryOne()
== null
+ assert from('TestingType').where('testingTypeId', keepId).queryOne()
!= null
+ }
+
+ @Test
+ void testWhereDeletesByNonPrimaryKeyFieldAlone() {
+ // The whole point of filtering to "all entity fields" rather than "PK
fields only": bulk
+ // delete by a real, non-PK field (e.g. a foreign key shared by
several rows) is a legitimate,
+ // already-shipped pattern elsewhere in this codebase (e.g.
QuoteAdjustment deleted by
+ // quoteId+quoteItemSeqId, neither of which is its actual PK,
quoteAdjustmentId). PK-only
+ // filtering would make this always throw instead.
+ String sharedDescription = 'EDB' +
UUID.randomUUID().toString().replace('-', '').take(16)
+ String matchIdA = 'EDB' + UUID.randomUUID().toString().replace('-',
'').take(16)
+ String matchIdB = 'EDB' + UUID.randomUUID().toString().replace('-',
'').take(16)
+ String otherId = 'EDB' + UUID.randomUUID().toString().replace('-',
'').take(16)
+ delegator.create('TestingType', [testingTypeId: matchIdA, description:
sharedDescription])
+ delegator.create('TestingType', [testingTypeId: matchIdB, description:
sharedDescription])
+ delegator.create('TestingType', [testingTypeId: otherId, description:
'Unrelated'])
+
+ int rowsDeleted = new EntityDeleteBuilder(delegator,
'TestingType').where([description: sharedDescription])
+
+ assert rowsDeleted == 2
+ assert from('TestingType').where('testingTypeId', matchIdA).queryOne()
== null
+ assert from('TestingType').where('testingTypeId', matchIdB).queryOne()
== null
+ assert from('TestingType').where('testingTypeId', otherId).queryOne()
!= null
+ }
+
+ @Test
+ void testWhereThrowsWhenMapHasNoRealEntityFields() {
+ String testingTypeId = 'EDB' +
UUID.randomUUID().toString().replace('-', '').take(16)
+ delegator.create('TestingType', [testingTypeId: testingTypeId,
description: 'Should survive'])
+
+ // None of these keys are real fields on TestingType -- like
WorkEffortServicesScript.groovy's
+ // real, pre-existing bug of reusing a map keyed by workEffortId
against WorkEffortAssoc, which
+ // has no such field. Must fail loudly rather than silently building a
condition-less delete
+ // that would remove every TestingType row.
+ Map noRealFields = [userLogin: 'system', locale: 'en_US']
+
+ ServiceErrorException exception = assertThrows(ServiceErrorException) {
+ new EntityDeleteBuilder(delegator,
'TestingType').where(noRealFields)
+ }
+ assert exception.message.contains('TestingType')
+
+ assert from('TestingType').where('testingTypeId',
testingTypeId).queryOne() != null
+ }
+
+}
diff --git
a/framework/service/src/test/groovy/org/apache/ofbiz/service/test/engine/EntityUpdateBuilderTest.groovy
b/framework/service/src/test/groovy/org/apache/ofbiz/service/test/engine/EntityUpdateBuilderTest.groovy
new file mode 100644
index 0000000000..b521eddb69
--- /dev/null
+++
b/framework/service/src/test/groovy/org/apache/ofbiz/service/test/engine/EntityUpdateBuilderTest.groovy
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ofbiz.service.test.engine
+
+import org.apache.ofbiz.base.util.UtilDateTime
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.service.engine.EntityUpdateBuilder
+import org.apache.ofbiz.testtools.JunitJupiterTest
+import org.apache.ofbiz.testtools.JupiterTestHelper
+import org.junit.jupiter.api.Test
+
+import java.sql.Timestamp
+
+/**
+ * First time : ./gradlew 'ofbiz -l readers=seed,seed-initial -l
delegator=test'
+ * ./gradlew 'ofbiz -t component=service -t suitename=servicetests -t
case=engine-entity-update-builder-tests'
+ */
+@JunitJupiterTest
+class EntityUpdateBuilderTest implements JupiterTestHelper {
+
+ @Test
+ void testFirstFiltersRawMapToEntityFieldsBeforeQuery() {
+ // A fresh id per run, not a fixed literal: this test writes via
delegator.create() directly
+ // (not through a service), so it isn't reliably covered by the
suite's transactional rollback
+ // -- a fixed id would collide with a prior run's leftover row.
+ String testingTypeId = 'EUB' +
UUID.randomUUID().toString().replace('-', '').take(16)
+ delegator.create('TestingType', [testingTypeId: testingTypeId,
description: 'Original'])
+
+ // A raw service parameters map -- like a Groovy service script's own
`parameters` -- carries
+ // keys (userLogin, locale) that are never real fields on TestingType,
alongside the real
+ // identifying field(s). queryOne() would narrow this to PK fields
automatically; first()'s
+ // queryFirst() must filter it to the entity's own field names itself,
or building the query
+ // condition on a nonexistent column blows up.
+ Map rawParameters = [testingTypeId: testingTypeId, userLogin:
'system', locale: 'en_US']
+
+ GenericValue updated = new EntityUpdateBuilder(delegator,
'TestingType')
+ .where(rawParameters)
+ .first()
+ .set([description: 'Updated'])
+
+ assert updated.description == 'Updated'
+ }
+
+ @Test
+ void testOrderByPicksMostRecentAmongMultipleMatches() {
+ String testingTypeId = 'EUB' +
UUID.randomUUID().toString().replace('-', '').take(16)
+ delegator.create('TestingType', [testingTypeId: testingTypeId,
description: 'Original'])
+
+ Timestamp older = UtilDateTime.nowTimestamp()
+ Timestamp newer = new Timestamp(older.time + 60000)
+ String olderTestingId = delegator.getNextSeqId('Testing')
+ delegator.create('Testing', [testingId: olderTestingId, testingTypeId:
testingTypeId,
+ testingDate: older, testingName:
'Older'])
+ String newerTestingId = delegator.getNextSeqId('Testing')
+ delegator.create('Testing', [testingId: newerTestingId, testingTypeId:
testingTypeId,
+ testingDate: newer, testingName:
'Newer'])
+
+ GenericValue updated = new EntityUpdateBuilder(delegator, 'Testing')
+ .where([testingTypeId: testingTypeId])
+ .orderBy('-testingDate')
+ .first()
+ .set([comments: 'Expired'])
+
+ assert updated.testingId == newerTestingId
+
+ GenericValue olderTesting = from('Testing').where('testingId',
olderTestingId).queryOne()
+ assert olderTesting.comments == null
+ }
+
+}
diff --git a/framework/service/testdef/servicetests.xml
b/framework/service/testdef/servicetests.xml
index 0ed26b72db..101c0893eb 100644
--- a/framework/service/testdef/servicetests.xml
+++ b/framework/service/testdef/servicetests.xml
@@ -38,6 +38,12 @@ under the License.
<test-case case-name="engine-tracker-tests">
<jupiter-test-suite
class-name="org.apache.ofbiz.service.test.engine.TrackerEngineTest"/>
</test-case>
+ <test-case case-name="engine-entity-update-builder-tests">
+ <jupiter-test-suite
class-name="org.apache.ofbiz.service.test.engine.EntityUpdateBuilderTest"/>
+ </test-case>
+ <test-case case-name="engine-entity-delete-builder-tests">
+ <jupiter-test-suite
class-name="org.apache.ofbiz.service.test.engine.EntityDeleteBuilderTest"/>
+ </test-case>
<!-- this case is failing, so commenting out by default until an automatic
fix can be found
<test-case case-name="service-lock-wait-timeout-retry-test">
<service-test service-name="testServiceLockWaitTimeoutRetry"/>