fix(db-postgres): ensure deletion of numbers and texts in upsertRow (#11787)
### What? This PR fixes an issue while using `text` & `number` fields with `hasMany: true` where the last entry would be unreachable, and thus undeletable, because the `transformForWrite` function did not track these rows for deletion. This causes values that should've been deleted to remain in the edit view form, as well as the db, after a submission. This PR also properly threads the placeholder value from `admin.placeholder` to `text` & `number` `hasMany: true` fields. ### Why? To remove rows from the db when a submission is made where these fields are empty arrays, and to properly show an appropriate placeholder when one is set in config. ### How? Adjusting `transformForWrite` and the `traverseFields` to keep track of rows for deletion. Fixes #11781 Before: [Editing---Post-dbpg-before--Payload.webm](https://github.com/user-attachments/assets/5ba1708a-2672-4b36-ac68-05212f3aa6cb) After: [Editing---Post--dbpg-hasmany-after-Payload.webm](https://github.com/user-attachments/assets/1292e998-83ff-49d0-aa86-6199be319937)
This commit is contained in:
@@ -98,6 +98,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
withinArrayOrBlockLocale,
|
||||
}: TraverseFieldsArgs): T => {
|
||||
const sanitizedPath = path ? `${path}.` : path
|
||||
const localeCodes =
|
||||
adapter.payload.config.localization && adapter.payload.config.localization.localeCodes
|
||||
|
||||
const formatted = fields.reduce((result, field) => {
|
||||
if (fieldIsVirtual(field)) {
|
||||
@@ -506,6 +508,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
if (field.type === 'text' && field?.hasMany) {
|
||||
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
|
||||
if (!textPathMatch) {
|
||||
result[field.name] =
|
||||
isLocalized && localeCodes
|
||||
? Object.fromEntries(localeCodes.map((locale) => [locale, []]))
|
||||
: []
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -545,6 +551,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
if (field.type === 'number' && field.hasMany) {
|
||||
const numberPathMatch = numbers[`${sanitizedPath}${field.name}`]
|
||||
if (!numberPathMatch) {
|
||||
result[field.name] =
|
||||
isLocalized && localeCodes
|
||||
? Object.fromEntries(localeCodes.map((locale) => [locale, []]))
|
||||
: []
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -606,10 +616,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
if (isLocalized && Array.isArray(table._locales)) {
|
||||
if (!table._locales.length && adapter.payload.config.localization) {
|
||||
adapter.payload.config.localization.localeCodes.forEach((_locale) =>
|
||||
(table._locales as unknown[]).push({ _locale }),
|
||||
)
|
||||
if (!table._locales.length && localeCodes) {
|
||||
localeCodes.forEach((_locale) => (table._locales as unknown[]).push({ _locale }))
|
||||
}
|
||||
|
||||
table._locales.forEach((localeRow) => {
|
||||
@@ -725,8 +733,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
return result
|
||||
}, dataRef)
|
||||
|
||||
if (Array.isArray(table._locales)) {
|
||||
|
||||
@@ -3,7 +3,13 @@ import type { FlattenedArrayField } from 'payload'
|
||||
import { fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
import type {
|
||||
ArrayRowToInsert,
|
||||
BlockRowToInsert,
|
||||
NumberToDelete,
|
||||
RelationshipToDelete,
|
||||
TextToDelete,
|
||||
} from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
@@ -20,6 +26,7 @@ type Args = {
|
||||
field: FlattenedArrayField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
numbersToDelete: NumberToDelete[]
|
||||
parentIsLocalized: boolean
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
@@ -28,6 +35,7 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
textsToDelete: TextToDelete[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
@@ -45,12 +53,14 @@ export const transformArray = ({
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
const newRows: ArrayRowToInsert[] = []
|
||||
@@ -104,6 +114,7 @@ export const transformArray = ({
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentTableName: arrayTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
@@ -112,6 +123,7 @@ export const transformArray = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@ import { fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
import type {
|
||||
BlockRowToInsert,
|
||||
NumberToDelete,
|
||||
RelationshipToDelete,
|
||||
TextToDelete,
|
||||
} from './types.js'
|
||||
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
@@ -20,6 +25,7 @@ type Args = {
|
||||
field: FlattenedBlocksField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
numbersToDelete: NumberToDelete[]
|
||||
parentIsLocalized: boolean
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
@@ -28,6 +34,7 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
textsToDelete: TextToDelete[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
@@ -43,12 +50,14 @@ export const transformBlocks = ({
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
data.forEach((blockRow, i) => {
|
||||
@@ -117,6 +126,7 @@ export const transformBlocks = ({
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentTableName: blockTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
@@ -125,6 +135,7 @@ export const transformBlocks = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
|
||||
@@ -29,11 +29,13 @@ export const transformForWrite = ({
|
||||
blocksToDelete: new Set(),
|
||||
locales: {},
|
||||
numbers: [],
|
||||
numbersToDelete: [],
|
||||
relationships: [],
|
||||
relationshipsToDelete: [],
|
||||
row: {},
|
||||
selects: {},
|
||||
texts: [],
|
||||
textsToDelete: [],
|
||||
}
|
||||
|
||||
// This function is responsible for building up the
|
||||
@@ -50,6 +52,7 @@ export const transformForWrite = ({
|
||||
fields,
|
||||
locales: rowToInsert.locales,
|
||||
numbers: rowToInsert.numbers,
|
||||
numbersToDelete: rowToInsert.numbersToDelete,
|
||||
parentIsLocalized,
|
||||
parentTableName: tableName,
|
||||
path,
|
||||
@@ -58,6 +61,7 @@ export const transformForWrite = ({
|
||||
row: rowToInsert.row,
|
||||
selects: rowToInsert.selects,
|
||||
texts: rowToInsert.texts,
|
||||
textsToDelete: rowToInsert.textsToDelete,
|
||||
})
|
||||
|
||||
return rowToInsert
|
||||
|
||||
@@ -5,7 +5,13 @@ import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
import type {
|
||||
ArrayRowToInsert,
|
||||
BlockRowToInsert,
|
||||
NumberToDelete,
|
||||
RelationshipToDelete,
|
||||
TextToDelete,
|
||||
} from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
@@ -51,6 +57,7 @@ type Args = {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
numbers: Record<string, unknown>[]
|
||||
numbersToDelete: NumberToDelete[]
|
||||
parentIsLocalized: boolean
|
||||
/**
|
||||
* This is the name of the parent table
|
||||
@@ -64,6 +71,7 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
textsToDelete: TextToDelete[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
@@ -86,6 +94,7 @@ export const traverseFields = ({
|
||||
insideArrayOrBlock = false,
|
||||
locales,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized,
|
||||
parentTableName,
|
||||
path,
|
||||
@@ -94,6 +103,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
if (row._uuid) {
|
||||
@@ -136,12 +146,14 @@ export const traverseFields = ({
|
||||
field,
|
||||
locale: localeKey,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
|
||||
@@ -159,12 +171,14 @@ export const traverseFields = ({
|
||||
data: data[field.name],
|
||||
field,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
@@ -202,12 +216,14 @@ export const traverseFields = ({
|
||||
field,
|
||||
locale: localeKey,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
}
|
||||
@@ -222,12 +238,14 @@ export const traverseFields = ({
|
||||
data: fieldData,
|
||||
field,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
@@ -257,6 +275,7 @@ export const traverseFields = ({
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentTableName,
|
||||
path: `${path || ''}${field.name}.`,
|
||||
@@ -265,6 +284,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
})
|
||||
@@ -287,6 +307,7 @@ export const traverseFields = ({
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentTableName,
|
||||
path: `${path || ''}${field.name}.`,
|
||||
@@ -295,6 +316,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
@@ -380,6 +402,11 @@ export const traverseFields = ({
|
||||
if (typeof fieldData === 'object') {
|
||||
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
if (!localeData.length) {
|
||||
textsToDelete.push({ locale: localeKey, path: textPath })
|
||||
return
|
||||
}
|
||||
|
||||
transformTexts({
|
||||
baseRow: {
|
||||
locale: localeKey,
|
||||
@@ -392,6 +419,11 @@ export const traverseFields = ({
|
||||
})
|
||||
}
|
||||
} else if (Array.isArray(fieldData)) {
|
||||
if (!fieldData.length) {
|
||||
textsToDelete.push({ locale: withinArrayOrBlockLocale, path: textPath })
|
||||
return
|
||||
}
|
||||
|
||||
transformTexts({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
@@ -412,6 +444,11 @@ export const traverseFields = ({
|
||||
if (typeof fieldData === 'object') {
|
||||
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
if (!localeData.length) {
|
||||
numbersToDelete.push({ locale: localeKey, path: numberPath })
|
||||
return
|
||||
}
|
||||
|
||||
transformNumbers({
|
||||
baseRow: {
|
||||
locale: localeKey,
|
||||
@@ -424,6 +461,11 @@ export const traverseFields = ({
|
||||
})
|
||||
}
|
||||
} else if (Array.isArray(fieldData)) {
|
||||
if (!fieldData.length) {
|
||||
numbersToDelete.push({ locale: withinArrayOrBlockLocale, path: numberPath })
|
||||
return
|
||||
}
|
||||
|
||||
transformNumbers({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
|
||||
@@ -23,6 +23,16 @@ export type RelationshipToDelete = {
|
||||
path: string
|
||||
}
|
||||
|
||||
export type TextToDelete = {
|
||||
locale?: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type NumberToDelete = {
|
||||
locale?: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type RowToInsert = {
|
||||
arrays: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
@@ -35,6 +45,7 @@ export type RowToInsert = {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
numbers: Record<string, unknown>[]
|
||||
numbersToDelete: NumberToDelete[]
|
||||
relationships: Record<string, unknown>[]
|
||||
relationshipsToDelete: RelationshipToDelete[]
|
||||
row: Record<string, unknown>
|
||||
@@ -42,4 +53,5 @@ export type RowToInsert = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
textsToDelete: TextToDelete[]
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: textsToInsert,
|
||||
rows: [...textsToInsert, ...rowToInsert.textsToDelete],
|
||||
tableName: textsTableName,
|
||||
})
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: numbersToInsert,
|
||||
rows: [...numbersToInsert, ...rowToInsert.numbersToDelete],
|
||||
tableName: numbersTableName,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
|
||||
admin: {
|
||||
className,
|
||||
description,
|
||||
placeholder,
|
||||
placeholder: placeholderFromProps,
|
||||
step = 1,
|
||||
} = {} as NumberFieldClientProps['field']['admin'],
|
||||
hasMany = false,
|
||||
@@ -126,6 +126,8 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
|
||||
|
||||
const styles = useMemo(() => mergeFieldStyles(field), [field])
|
||||
|
||||
const placeholder = getTranslation(placeholderFromProps, i18n)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
@@ -174,7 +176,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
|
||||
// numberOnly
|
||||
onChange={handleHasManyChange}
|
||||
options={[]}
|
||||
placeholder={t('general:enterAValue')}
|
||||
placeholder={placeholder}
|
||||
showError={showError}
|
||||
value={valueToRender as Option[]}
|
||||
/>
|
||||
@@ -191,7 +193,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
|
||||
// @ts-expect-error
|
||||
e.target.blur()
|
||||
}}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
placeholder={placeholder}
|
||||
step={step}
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
onChange,
|
||||
onKeyDown,
|
||||
path,
|
||||
placeholder,
|
||||
placeholder: placeholderFromProps,
|
||||
readOnly,
|
||||
required,
|
||||
rtl,
|
||||
@@ -91,6 +91,8 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = getTranslation(placeholderFromProps, i18n)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
@@ -143,7 +145,7 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
}}
|
||||
onChange={onChange}
|
||||
options={[]}
|
||||
placeholder={t('general:enterAValue')}
|
||||
placeholder={placeholder}
|
||||
showError={showError}
|
||||
value={valueToRender}
|
||||
/>
|
||||
@@ -155,7 +157,7 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
name={path}
|
||||
onChange={onChange as (e: ChangeEvent<HTMLInputElement>) => void}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value || ''}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
checkboxFieldsSlug,
|
||||
collapsibleFieldsSlug,
|
||||
groupFieldsSlug,
|
||||
numberFieldsSlug,
|
||||
relationshipFieldsSlug,
|
||||
tabsFieldsSlug,
|
||||
textFieldsSlug,
|
||||
@@ -401,6 +402,31 @@ describe('Fields', () => {
|
||||
|
||||
expect(resInSecond.totalDocs).toBe(1)
|
||||
})
|
||||
|
||||
it('should delete rows when updating hasMany with empty array', async () => {
|
||||
const { id: createdDocId } = await payload.create({
|
||||
collection: textFieldsSlug,
|
||||
data: {
|
||||
text: 'hasMany deletion test',
|
||||
hasMany: ['one', 'two', 'three'],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: textFieldsSlug,
|
||||
id: createdDocId,
|
||||
data: {
|
||||
hasMany: [],
|
||||
},
|
||||
})
|
||||
|
||||
const resultingDoc = await payload.findByID({
|
||||
collection: textFieldsSlug,
|
||||
id: createdDocId,
|
||||
})
|
||||
|
||||
expect(resultingDoc.hasMany).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationship', () => {
|
||||
@@ -1042,6 +1068,30 @@ describe('Fields', () => {
|
||||
|
||||
expect(numbersNotExists.docs).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should delete rows when updating hasMany with empty array', async () => {
|
||||
const { id: createdDocId } = await payload.create({
|
||||
collection: numberFieldsSlug,
|
||||
data: {
|
||||
localizedHasMany: [1, 2, 3],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: numberFieldsSlug,
|
||||
id: createdDocId,
|
||||
data: {
|
||||
localizedHasMany: [],
|
||||
},
|
||||
})
|
||||
|
||||
const resultingDoc = await payload.findByID({
|
||||
collection: numberFieldsSlug,
|
||||
id: createdDocId,
|
||||
})
|
||||
|
||||
expect(resultingDoc.localizedHasMany).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should query hasMany within an array', async () => {
|
||||
|
||||
Reference in New Issue
Block a user