Skip to content

Commit 5279304

Browse files
authored
Merge pull request #1442 from nextcloud-libraries/fix/column
fix(column): validate interface rather than the instance type
2 parents eaac8df + 6a3a582 commit 5279304

4 files changed

Lines changed: 65 additions & 48 deletions

File tree

__tests__/view.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ describe('Invalid View creation', () => {
8181
).toThrowError('View order must be a number')
8282
})
8383
test('Invalid columns', () => {
84+
expect(() => new View({
85+
id: 'test',
86+
name: 'Test',
87+
order: 1,
88+
icon: '<svg></svg>',
89+
getContents: () => Promise.reject(new Error()),
90+
columns: [{}],
91+
} as unknown as View),
92+
).toThrowError('A column id is required')
93+
})
94+
test('Invalid columns with null', () => {
8495
expect(() => new View({
8596
id: 'test',
8697
name: 'Test',
@@ -89,7 +100,7 @@ describe('Invalid View creation', () => {
89100
getContents: () => Promise.reject(new Error()),
90101
columns: [null],
91102
} as unknown as View),
92-
).toThrowError('View columns must be an array of Column. Invalid column found')
103+
).toThrowError('View column must be an object')
93104
})
94105
test('Invalid emptyView', () => {
95106
expect(() => new View({

lib/navigation/column.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import type { INode } from '../node/node.ts'
77
import type { IView } from './view.ts'
88

9+
import { checkOptionalProperty } from '../utils/objectValidation.ts'
10+
911
interface ColumnData {
1012
/** Unique column ID */
1113
id: string
@@ -27,7 +29,7 @@ export class Column implements ColumnData {
2729
private _column: ColumnData
2830

2931
constructor(column: ColumnData) {
30-
isValidColumn(column)
32+
validateColumn(column)
3133
this._column = column
3234
}
3335

@@ -54,13 +56,16 @@ export class Column implements ColumnData {
5456
}
5557

5658
/**
57-
* Typescript cannot validate an interface.
58-
* Please keep in sync with the Column interface requirements.
59+
* Validate a column definition
5960
*
60-
* @param {ColumnData} column the column to check
61-
* @return {boolean} true if the column is valid
61+
* @param column - the column to check
62+
* @throws {Error} if the column is not valid
6263
*/
63-
const isValidColumn = function(column: ColumnData): boolean {
64+
export function validateColumn(column: ColumnData): void {
65+
if (typeof column !== 'object' || column === null) {
66+
throw new Error('View column must be an object')
67+
}
68+
6469
if (!column.id || typeof column.id !== 'string') {
6570
throw new Error('A column id is required')
6671
}
@@ -74,13 +79,6 @@ const isValidColumn = function(column: ColumnData): boolean {
7479
}
7580

7681
// Optional properties
77-
if (column.sort && typeof column.sort !== 'function') {
78-
throw new Error('Column sortFunction must be a function')
79-
}
80-
81-
if (column.summary && typeof column.summary !== 'function') {
82-
throw new Error('Column summary must be a function')
83-
}
84-
85-
return true
82+
checkOptionalProperty(column, 'sort', 'function')
83+
checkOptionalProperty(column, 'summary', 'function')
8684
}

lib/navigation/view.ts

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import type { IFolder, INode } from '../node/index.ts'
77

88
import isSvg from 'is-svg'
9-
import { Column } from './column.ts'
9+
import { Column, validateColumn } from './column.ts'
10+
import { checkOptionalProperty } from '../utils/objectValidation.ts'
1011

1112
export type ContentsWithRoot = {
1213
folder: IFolder,
@@ -241,37 +242,13 @@ export function validateView(view: IView) {
241242
checkOptionalProperty(view, 'sticky', 'boolean')
242243

243244
if (view.columns) {
244-
view.columns.forEach((column) => {
245-
if (!(column instanceof Column)) {
246-
throw new Error('View columns must be an array of Column. Invalid column found')
247-
}
248-
})
249-
}
250-
}
251-
252-
/**
253-
* Check an optional property type
254-
*
255-
* @param obj - the object to check
256-
* @param property - the property name
257-
* @param type - the expected type
258-
* @throws {Error} if the property is defined and not of the expected type
259-
*/
260-
function checkOptionalProperty(
261-
obj: Partial<IView>,
262-
property: keyof IView,
263-
type: 'array' | 'function' | 'string' | 'boolean' | 'number' | 'object',
264-
): void {
265-
if (typeof obj[property] !== 'undefined') {
266-
if (type === 'array') {
267-
if (!Array.isArray(obj[property])) {
268-
throw new Error(`View ${property} must be an array`)
269-
}
270-
// eslint-disable-next-line valid-typeof
271-
} else if (typeof obj[property] !== type) {
272-
throw new Error(`View ${property} must be a ${type}`)
273-
} else if (type === 'object' && (obj[property] === null || Array.isArray(obj[property]))) {
274-
throw new Error(`View ${property} must be an object`)
245+
// we cannot use `instanceof` here because if the Navigation and the Column class are loaded by different apps
246+
// (Navigation is set by files app and Column by a 3rd party app),
247+
// the `instanceof` check will fail even if the object has the correct shape because they are different classes in memory.
248+
view.columns.forEach(validateColumn)
249+
const columnIds = view.columns.reduce((set, column) => set.add(column.id), new Set<string>())
250+
if (columnIds.size !== view.columns.length) {
251+
throw new Error('View columns must have unique ids')
275252
}
276253
}
277254
}

lib/utils/objectValidation.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Check an optional property type
8+
*
9+
* @param obj - the object to check
10+
* @param property - the property name
11+
* @param type - the expected type
12+
* @throws {Error} if the property is defined and not of the expected type
13+
*/
14+
export function checkOptionalProperty<T extends object>(
15+
obj: T,
16+
property: Exclude<keyof T, symbol>,
17+
type: 'array' | 'function' | 'string' | 'boolean' | 'number' | 'object',
18+
): void {
19+
if (typeof obj[property] !== 'undefined') {
20+
if (type === 'array') {
21+
if (!Array.isArray(obj[property])) {
22+
throw new Error(`View ${property} must be an array`)
23+
}
24+
// eslint-disable-next-line valid-typeof
25+
} else if (typeof obj[property] !== type) {
26+
throw new Error(`View ${property} must be a ${type}`)
27+
} else if (type === 'object' && (obj[property] === null || Array.isArray(obj[property]))) {
28+
throw new Error(`View ${property} must be an object`)
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)