Skip to content

Commit 6a3a582

Browse files
committed
fix(column): validate interface rather than the instance type
We cannot use `instanceof` because two apps will have different libraries so the prototype is not the same and the check will always fail. Instead validate the column using the already existing validation method. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent eaac8df commit 6a3a582

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)